Async
The tasks a running service holds, the way one delivery passes through them, and the order a shutdown stops them in.
The diagram as text
- App runtime: Tokio, multi-thread by default: default one task runs every step; workers(n) one loop task hands them out; threads(n) one feeder task fills the rings; n worker tasks an inbox of one; n rings of eight slots; Tracked tasks: and_after, hooks, retry_after timers
- Subscription streams of the connected brokers → default one task runs every step: poll_next
- Subscription streams of the connected brokers → workers(n) one loop task hands them out
- Subscription streams of the connected brokers → threads(n) one feeder task fills the rings
- workers(n) one loop task hands them out → n worker tasks an inbox of one: send
- threads(n) one feeder task fills the rings → n rings of eight slots: push
- n rings of eight slots → n OS threads a Tokio runtime on each: pop
- default one task runs every step → Tracked tasks: and_after, hooks, retry_after timers: spawn
- n worker tasks an inbox of one → Tracked tasks: and_after, hooks, retry_after timers
The service runs on Tokio. #[ruststream::app] builds a
multi-thread runtime. flavor = "current_thread" picks a single-threaded one, and
worker_threads = n sets the number of threads.
A subscriber is a Stream of deliveries, and back-pressure comes from polling it. The default
loop polls for the next delivery once the previous one is settled.
workers(n) keeps at most n deliveries in
flight. workers(n, by_key) sends every delivery of one key to the same worker, so one key keeps
its order. threads(n) reads ahead at most
n × 9 deliveries: one in the hands of each thread and eight in its ring.
A dedicated thread runs the whole delivery, from decode to settlement, on its own current-thread
runtime. Work the handler hands to MainRuntime, taken as
Ctx<MainRuntime>, runs back on the app runtime.
One delivery
The diagram as text
- Loop task: poll the stream; poll the next one
- Handling task: build the context; consume layers; decode; handler body; publish the reply; settle
- Later, tracked: continuations and timers
- poll the stream → build the context
- build the context → consume layers
- consume layers → decode
- decode → handler body
- handler body → publish the reply
- publish the reply → settle
- settle → poll the next one: next turn
- settle → continuations and timers: spawn
The handling task is the loop task itself by default, a worker task under workers(n), and a
dedicated thread under threads(n). An and_after
continuation, a post-settle hook and a retry_after
timer run after the settlement, as tasks the app tracks. They run at most once: the delivery is
already settled.
Shutdown
The diagram as text
- A signal, a fail-fast or RunningApp::shutdown → on_shutdown hooks run while the brokers are connected
- on_shutdown hooks run while the brokers are connected → The loops stop taking new deliveries
- The loops stop taking new deliveries → Deliveries in hand are handled and settled
- Deliveries in hand are handled and settled → Tracked continuations and retry timers finish
- Tracked continuations and retry timers finish → Brokers shut down in reverse registration order
- Brokers shut down in reverse registration order → after_shutdown hooks run with the brokers closed
A shutdown timeout bounds the wait. When it
expires, loops still running are aborted, and tracked tasks still pending are left to finish on
their own. Without a timeout, a shutdown waits for all of them. A task spawned with plain
tokio::spawn on a dedicated thread is not tracked and ends with its thread.