Router¶
A Router gives clients one server address and selects an available compute node for each request. A node supervisor starts, checks and restarts its local Python server. The supervisor and Python server initiate their connections to the Router, so compute nodes do not need to accept inbound network connections.
Install the client and Python server from the same revision as the Rust binaries so their internal protocols match.
Components and connections¶
Component |
Where it runs |
Responsibility |
|---|---|---|
|
A host reachable by clients and nodes |
Accept HTTP requests, manage a bounded wait queue and select nodes |
|
Each compute node |
Supervise one Python server process tree and report its health |
Python server |
Each compute node |
Execute programs with the existing worker pool |
HTTP is the public request-and-response protocol. Internally, gRPC is the streaming remote-call protocol connecting each node to the Router. It uses Protocol Buffers, a binary message format, over HTTP/2.
The node manager opens ConnectSupervisor, a stream of health, instance and
capacity reports. The Python server opens one ConnectSlot stream per local
worker. A slot is one connection able to carry one execution at a time. Both
connections originate on the node; the Router sends work over these existing
streams. The node manager never handles execution bodies.
Request and response bytes travel in frames of at most 256 KiB, where KiB means
1024 bytes. Small bounded queues and HTTP/2 flow control limit buffering in the
tunnel. The Python /execute implementation still assembles the complete
request before parsing it.
Build and launch¶
Use Rust 1.87 or newer and Cargo, Rust’s build tool. The build supplies its own Protocol Buffers compiler. Build from the repository root:
cargo build --release --locked
export PATH="$PWD/target/release:$PATH"
Install the Python package on the Router host and the worker environment on each compute node. Start the Router:
export KCORAL_NODE_TOKEN='<shared-node-token>'
kcoral router --host 127.0.0.1 --port 9000
On the same machine, start a node with the same token and a stable node-id:
export KCORAL_NODE_TOKEN='<shared-node-token>'
kcoral server \
--router http://127.0.0.1:9000 \
--node-id gpu-a
The examples run on one machine. For remote nodes, bind the Router with
--host 0.0.0.0 and replace 127.0.0.1 with its reachable address. Give each node
a distinct stable node-id. The example uses
plain HTTP for a controlled network; an HTTPS endpoint requires TLS (transport
encryption) terminated by a compatible proxy. The optional token authenticates
node streams; it does not provide public client authorization or encryption.
With --router, kcoral server starts a supervisor (the node manager) that
manages the Python server using the current Python environment. Its health-check address follows
--host and --port; the Router does not connect to that address.
Without --router, the Python server runs directly.
Pass server options directly:
kcoral server \
--router http://127.0.0.1:9000 \
--node-id gpu-a \
--host 0.0.0.0 --gpus 0 --log-dir /var/log/kcoral
The manager requires Linux 5.3 or newer, access to /proc, and permission to
signal its child processes. Run the Router
and node managers under your service manager so those processes are also
restarted if they fail.
Clients continue using Client, Program, POST /execute and GET /health:
from kcoral import Client
with Client("http://127.0.0.1:9000") as client:
print(client.target())
# client.execute(program) uses the same program format as a direct server.
Capacity and health¶
A node needs a healthy supervisor report and an idle slot for the same Python server instance before it can receive work. All eligible nodes must match the Router’s target and runtime-version baseline. A Python restart invalidates slots from the previous instance. Disconnected nodes without active requests expire after the retention interval; once all records expire, the next healthy node establishes a new compatibility baseline.
When choosing between nodes, the Router compares reported busy workers with its own active requests and divides the larger number by worker capacity. It samples two candidates and chooses the lower load, breaking ties toward the least recently selected node. Python’s worker pool remains the final capacity boundary.
Router option |
Default |
Meaning |
|---|---|---|
|
|
Router listen address |
|
|
Interval for checking status freshness |
|
|
Maximum age of a node health report |
|
|
Failed observations before removing a ready node |
|
|
Successful observations before a failed node returns |
|
|
Maximum wait for capacity |
|
|
Bound on requests waiting for capacity |
|
|
Retention of disconnected, unused node records |
|
|
Maximum accepted request body size |
The Router uses the health schema, with router-local request counts.
The supervisor polls loopback-only /internal/worker-status and forwards worker
occupancy and environment information to the router. Node eligibility follows
these states:
Node status |
Meaning |
|---|---|
|
A node is registered but has not established healthy service |
|
Health and compatibility allow admission, subject to capacity |
|
Successful observations are accumulating after a failure |
|
Target or runtime versions differ from the Router baseline |
|
Health observations failed or became stale |
Start with one Router. With multiple replicas, direct each node’s supervisor stream and all of its slots to the same replica. Otherwise a replica may see health without the corresponding execution connections.
Cache negotiation and failures¶
X-KCoral-Node identifies the selected node. The client sends it back
as a preference during cache-miss retries. If the node or Python instance
changes, the client may need to resend bytes. Memory caches are instance-local;
persistent file caches follow their configured storage lifetime.
The Router creates a fresh X-Request-ID for every HTTP attempt, including
rejections, and propagates it through Router and Python logs. Cache negotiation
may therefore produce several request identifiers for one Client.execute().
Router and node-manager logs are plain text without terminal color codes; the
Router’s request_finished record can be correlated with Python’s JSON events
using request_id.
Failure |
Client-visible behavior |
|---|---|
Queue full or no capacity before timeout |
HTTP 503 before node selection; retrying is safe |
Tunnel fails after work may have reached a node |
HTTP 502 with an unknown execution outcome; the Router does not replay the program |
Client disconnects |
Router capacity is released and the slot is discarded; this does not prove GPU computation has stopped |
Router connection fails |
Node connections reconnect; local server health checks and restart decisions continue independently |
The node manager checks local health every 2 seconds by default, with a 1-second probe timeout, 3-failure threshold and 30-second startup grace. Restart delays grow from 1 to 30 seconds with jitter, and reset after 60 seconds of stable running.
To stop a node, send SIGTERM, the normal termination signal, to the kcoral server process.
It withdraws healthy status and signals the Python child. Idle slots close and
active requests finish before the child exits. Normal shutdown waits for that
exit without imposing an additional timeout.
When restarting an unhealthy server, --termination-grace-seconds (default 5)
limits the wait after SIGTERM before forced cleanup. The node manager removes
descendant processes before starting the replacement Python server.
External shutdown deadlines can still interrupt work. Configure the service
manager’s stop timeout, such as systemd’s TimeoutStopSec, to allow the desired
request completion. The service manager must also clean up the entire process
tree if the node manager itself dies.
Implementation reference: Router source and package guide.