Metadata-Version: 2.4
Name: granian-multiport
Version: 0.1.1
Summary: Run each Granian worker on its own TCP port
Author: Inada Naoki
License-Expression: MIT
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: granian<2.9,>=2.8.0
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Dynamic: license-file

# granian-multiport

`granian-multiport` runs each Granian worker on a different TCP port.  It is
intended for deployments where a reverse proxy such as nginx performs
load-aware dispatch (`least_conn`) across workers.

Every listening socket is bound once in the parent process and inherited by
exactly one worker.

Granian itself is not patched.  This package reuses Granian's complete Click
CLI and supplies an external `MPServer` subclass that binds a distinct listener
for every worker.


## Why

Granian uses `SO_REUSEPORT` to load-balance multiple workers, but Linux distributes connections randomly with `SO_REUSEPORT`. This means that when distributing 16 connections across 8 workers, there is an approximately 80% chance that one worker will receive 4 or more connections. This can lead to issues with 99%-tile latency and CPU utilization efficiency.

By assigning different ports to each worker process and allowing a reverse proxy such as nginx to implement a better load-balancing algorithm, 99%-tile latency and CPU utilization efficiency can be significantly improved.

In addition, Granian relies on `net.ipv4.tcp_migrate_req` for graceful restarts when `SO_REUSEPORT` is enabled. In environments other than Linux, or in environments where this system setting is not configured, connections remaining in the backlog of old workers will be discarded and errors will occur when restarting workers.

This project aims to demonstrate that better load balancing can be achieved through multi-port support.


## Install

```console
python -m pip install .
```

The initial release supports Granian 2.8.x and Python 3.12 or newer on POSIX
systems.  It intentionally pins the Granian minor series because it uses the
private multiprocessing server API.

## Run on consecutive ports

The usual Granian `--port` becomes the first worker port:

```console
granian-multiport \
  --interface asgi \
  --host 127.0.0.1 \
  --port 8000 \
  --workers 4 \
  myproject.asgi:application
```

Workers listen on ports 8000, 8001, 8002, and 8003.  Every other Granian CLI
option remains available.

## Run on explicit ports

```console
granian-multiport \
  --interface asgi \
  --ports 8000,8010,8020,8030 \
  myproject.asgi:application
```

When both `--ports` and `--workers` are given, their counts must agree.  The
explicit list can also be supplied through `GRANIAN_MULTIPORT_PORTS`.
Unix domain sockets are intentionally unsupported because the command's purpose
is to expose one TCP endpoint per worker.

## nginx

```nginx
upstream app_workers {
    least_conn;
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
    server 127.0.0.1:8003;

    keepalive 32;
}

server {
    listen 80;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_pass http://app_workers;
    }
}
```

With upstream keep-alive enabled, nginx's `least_conn` choice occurs when it
opens or selects an upstream connection; benchmark with the connection reuse
policy that matches production.

## Development

```console
python -m pip install -e '.[test]'
pytest
```

The integration test starts three workers, requests every port, and verifies
that each port consistently reaches a different worker process.
ll
