Tune the adaptive I/O throttle on a PUSH/PULL stream
For Rust developers running rzmq under uneven traffic who want to see and change how each connection shares its time between sending and receiving
You will stream 1,000 readings from a PUSH source to a PULL sink over TCP on localhost and read the adaptive throttle's state on both ends. Then you give the sink its own throttle settings, see what happens when they arrive too late and switch the throttle off on the source.
Before you start
You need Rust with Cargo. rzmq comes from crates.io:
rzmq = "0"Set up the project#
Start a new binary crate.
Add rzmq and Tokio:
[]
= "0"
= { = "1", = ["full"] }The programs use Tokio's single-threaded current_thread runtime, which keeps the numbers they print the same from run to run.
Read the throttle's settings#
Every rzmq connection on the default backend runs an adaptive I/O throttle. It keeps a balance per connection: each incoming message adds credit_per_message and each outgoing message subtracts it. It learns the normal balance over time. When the live balance drifts too far from it, the throttle sometimes yields to the Tokio scheduler so the direction that is falling behind gets to run. A connection that mostly sends can't starve its own receiving that way.
A socket's throttle settings live in the ADAPTIVE_THROTTLE option as an AdaptiveThrottleSocketConfig. get_option returns it encoded and FromBytes decodes it. Replace src/main.rs:
use FromBytes;
use ;
use ;
async
These are the defaults every socket starts with:
credit_per_message: how far one message moves the balance.healthy_balance_width: the half-width of the zone around the learned balance where the throttle never yields.max_imbalance: how far past that zone the chance of yielding reaches 100%.yield_after_n_consecutive: forces a yield after that many operations in one direction.nudge_interval_opsandadaptive_learning_rate: how often and how fast the learned balance follows the live one.strategy: the curve from imbalance to the chance of yielding,ThrottleStrategy::PowerCurve { exponent }orThrottleStrategy::Linear.priority: the direction to favour.ThrottlePriority::AutopicksEgressfor a connection the socket accepted andIngressfor one it opened.Egress,IngressandNoneset it outright.priority_boost_factor: multiplies the chance of yielding while doing the non-priority direction during an imbalance.enabled: whether the throttle runs at all.
Watch the throttle on a stream#
ADAPTIVE_THROTTLE_STATS is read-only. It returns one ThrottleStats per connection, with the priority the connection settled on and its live balance. A stream only moves one way, so the balance on each end runs away from zero in its own direction.
The source binds and the sink connects. Replace src/main.rs:
use FromBytes;
use ;
use ;
const READINGS: &str = "tcp://127.0.0.1:5680";
const READING_COUNT: u32 = 1_000;
async
async
async
sink received 1000 readings
source: priority Egress, balance -5000
sink: priority Ingress, balance 5000
The source accepted its connection, so Auto resolved to Egress. The sink opened its connection and got Ingress. Each side moved 1,000 messages at the default credit_per_message of 5, outgoing on the source and incoming on the sink. The stats are read after the sink has every reading, so both connections have finished their work and the numbers are settled.
Give the sink its own settings#
To change the settings, build an AdaptiveThrottleSocketConfig and set it as ADAPTIVE_THROTTLE. Start from ..Default::default() and name only the fields you want to change. Here the sink counts each message as 1, yields after 64 operations in a row, uses the linear curve and favours neither direction. Replace src/main.rs:
use FromBytes;
use
;
use ;
const READINGS: &str = "tcp://127.0.0.1:5680";
const READING_COUNT: u32 = 1_000;
async
async
async
sink throttle: credit 1, yield after 64, Linear, priority None
sink received 1000 readings
source: priority Egress, balance -5000
sink: priority None, balance 1000
Reading the option back returns what you set. The sink's connection now runs with priority None and its balance grew by 1 per reading. The source kept its defaults.
Socket::with_throttle_config sets the same option while you build the socket:
let sink = ctx
.socket?
.with_throttle_config
.await?;Set it before bind or connect#
A connection takes its throttle settings when it starts. Once a socket has called bind or connect, setting ADAPTIVE_THROTTLE returns ZmqError::InvalidState. Add this after sink.connect:
match sink.set_option.await
sink throttle: credit 1, yield after 64, Linear, priority None
too late: ADAPTIVE_THROTTLE must be set before bind or connect
sink received 1000 readings
source: priority Egress, balance -5000
sink: priority None, balance 1000
The sink keeps the settings it had when it connected.
Switch the throttle off#
ADAPTIVE_THROTTLE also takes 1i32 or 0i32. That changes enabled and leaves the other fields as they are. A disabled throttle does no work on the connection's hot path, which suits a socket that only ever moves data one way. Turn it off on the source before it binds:
let source = ctx.socket?;
source.set_option.await?;
println!;
source.bind.await?;source throttle enabled: false
sink throttle: credit 1, yield after 64, Linear, priority None
too late: ADAPTIVE_THROTTLE must be set before bind or connect
sink received 1000 readings
source: priority Egress, balance 0
sink: priority None, balance 1000
The source's connection is still listed, but its balance stays at 0 because the throttle no longer counts anything. The readings arrive the same either way. The throttle only decides when a connection yields to the scheduler.
The finished program:
use FromBytes;
use
;
use ;
const READINGS: &str = "tcp://127.0.0.1:5680";
const READING_COUNT: u32 = 1_000;
async
async
async
Sockets on the io_uring backend#
io_uring sockets do not support the adaptive throttle. A socket with IO_URING_SESSION_ENABLED set runs its TCP connections on the io_uring backend, which has no throttle. ADAPTIVE_THROTTLE settings have no effect on those connections and ADAPTIVE_THROTTLE_STATS does not list them.
Shut down cleanly#
The program ends with ctx.term():
ctx.term.await?;The sink has read every reading by then, so nothing is left in flight. term() closes every socket made from the context and waits for their background tasks.
Where to go next#
The PUSH/PULL tutorial covers high-water marks and send timeouts. The DEALER/ROUTER tutorial builds a connection that carries traffic both ways.
The rzmq usage guide on GitHub has the adaptive throttle chapter and the rest of the socket options. The full API reference is on docs.rs.
