Expand description
§ergon-io
Network I/O for exchange clients on one busy-spinning thread: WebSocket and HTTP/1.1 over TLS 1.3.
- One non-blocking
pollmoves bytes between your loop and the kernel. - The kernel interface (epoll or
io_uring, with or without NIC polling) is configuration, chosen at run time. - TLS records are decrypted in place and messages parsed in place: one copy per byte.
AI assistance. Like the rest of ergon, this crate was written with heavy AI assistance. See AI-ASSISTANCE.md. It is not production-ready.
cargo add ergon-io --features webpki-rootsWhich features to pick: see Features, below.
§When to use it
| Use it for | Use something else for |
|---|---|
| A Linux thread that spins on its own core and owns 1 to a few hundred exchange connections (pick the backend by count: below) | An async application: tokio or mio |
| A loop that must never sleep in the kernel | Production on macOS or Windows: the driver is Linux-only |
| Fixed endpoints: HTTP/1.1 keep-alive, TLS 1.3 | HTTP/2 or 3, proxies, redirects, cookies, compression: the curl feature or a general client |
§Parts
| Part | What it does |
|---|---|
Driver | Owns the connections. Each poll makes one pass (receive, decrypt, decode, deliver, send) and returns. |
WsConn | WebSocket client. WsHandler::on_message gets a slice of the receive buffer. |
WsOptions | WebSocket limits and timers. |
Reconnect | Optional. Reopens closed connections with backoff; you resubscribe. |
HttpClient | A pool of warm HTTP connections behind one API: native (fast) or libcurl (feature curl). |
HttpConn | One HTTP/1.1 keep-alive connection. |
Signer | Binance-style request signing (HMAC-SHA256 or Ed25519), no allocation. |
§Example: a WebSocket feed
Connect, subscribe, handle each message, and reconnect and subscribe again
after a drop (Linux, feature webpki-roots):
use ergon_io::reconnect::Retry;
use ergon_io::{ConnId, Error, Handler, Reconnect, RxMeta, WsDial, WsHandler};
/// The application: `Driver::poll` calls it.
struct App {
links: Reconnect<WsDial>, // reopens the connection with backoff
opened: Vec<ConnId>, // subscribe on these once `poll` returns
}
impl Handler for App {
fn on_open(&mut self, id: ConnId) {
if self.links.on_open(id).is_some() {
self.opened.push(id); // first connect or reconnect
}
}
fn on_closed(&mut self, id: ConnId, error: Error) {
eprintln!("closed: {error}");
self.links.on_closed(id); // schedules the reconnect
}
}
impl WsHandler for App {
fn on_message(&mut self, _: ConnId, _: u8, payload: &[u8], _: &RxMeta, _: u16) {
// `payload` points into the receive buffer: decode it here.
println!("{}", String::from_utf8_lossy(payload));
}
}
#[cfg(all(target_os = "linux", feature = "webpki-roots"))]
fn main() -> ergon_io::Result<()> {
use ergon_io::{Config, Driver, Target, WsConn, tls};
let tls = tls::client_config(tls::webpki_roots())?;
let target = Target::new("stream.binance.com", 9443, "/ws");
let mut app = App { links: Reconnect::new(Retry::default())?, opened: Vec::new() };
app.links.add(move || WsDial::new(target.clone(), tls.clone(), 1 << 20));
// Pin this thread to an isolated core first: ergon-io never pins it.
let mut driver: Driver<WsConn> = Driver::new(Config::parse("recv-busy")?)?;
loop {
driver.poll(&mut app)?; // receive, decrypt, deliver, send
app.links.poll(&mut driver); // connect, or reconnect when due
for id in app.opened.drain(..) {
if let Some(mut ws) = driver.get_mut(id)
&& !ws.is_closing()
{
ws.send_text(r#"{"method":"SUBSCRIBE","params":["btcusdt@trade"],"id":1}"#)?;
}
}
}
}
#[cfg(not(all(target_os = "linux", feature = "webpki-roots")))]
fn main() {}- The full example, with a clean close: io/examples/websocket.rs. A test runs it against a local server on every backend.
- Run it from a checkout of the repository:
cargo run -p ergon-io --features webpki-roots --example websocket.
§Defaults
TCP_NODELAY on, keepalive and TCP_USER_TIMEOUT set, a 4 MiB receive
buffer, WebSocket Pings after 10 s quiet, reconnect off. Every default and
how to change it: the book’s
defaults.
§Guarantees and limits
- Never sleeps with
epoll-spin,recv-busy,uring-spinoruring-sqpoll.epoll-busyanduring-napimay wait up towait_us. - One failure closes one connection. It goes to
Handler::on_closed; the others keep running.pollfails only when the backend fails. - Connecting does not block.
Driver::connecttakes a step per poll, but the TLS handshake makes one poll long;Driver::connect_backgroundruns it on a helper thread. - Timers stay off the hot path. Pings and timeouts run on the driver’s deadline check (every 250 µs), never on the read or send path.
- TLS 1.3 only. TLS 1.2 is refused with a clear error.
- Client side only. No listener.
- WebSocket, not supported: compression (
permessage-deflate), other extensions, proxies, sending fragmented messages. - Linux for the driver.
DriverandHttpClientare Linux-only; the protocol code builds everywhere. - Kernel.
uring-spinneeds Linux 6.1+;uring-sqpoll,uring-napiandepoll-busyneed 6.9+. Theio_uringbackends need theuringfeature (on by default).prefer_busy_pollonrecv-busyand a raisedbusy_poll_budgetneedCAP_NET_ADMIN. - Host checks at start-up (ergon-tune):
the loop’s CPU, the receive buffer, each route’s quick ACK and each egress
NIC. Each finding goes to stderr once, with its fix, from a warning thread.
ERGON_HOST_CHECKS=offsilences those warning lines (requiremakes a warning refuse the start); the results stay onDriver::host_report(). - Measured on x86 (AWS
c7i, AzureD8ds_v6) and Graviton (AWSc8g): io/benches/BENCHMARK.md.
§Choosing a backend
Config::parse("backend[,key=value...]"). Count every socket the loop
thread polls (feeds, order connections, HttpClient pool connections):
- 1–4 sockets:
recv-busy. - More:
uring-sqpollwith a spare CPU, orrecv-busy,per_queue=trueon one CPU. - The interrupt backends (
epoll-spin,uring-spin) were level withuring-sqpollfrom 4 sockets with node-level NIC settings: moderation off and the NIC’s interrupts on a core of their own.
The full table, the evidence and the node settings: the book’s choosing a backend.
§Rate limits and errors
limit::Budgetchecks each request against the venue’s limits before it goes out: up to 8 buckets, all or none. It never sleeps or queues.- A failed request says whether it left (
outcome::Sent); a response maps to oneoutcome::Verdict. - A ban halts its scope until a person calls
Budget::clear_halt. - Send again only what did not run, inside its deadline
(
Budget::try_resend); look up an unknown order by its client order id. - One sending process per account and IP: the budget cannot see others.
- Binance, Bybit and OKX adapters:
io/examples/venues.rs(repository only). Venue limits, the error policy and the cost: the book’s rate limits and errors.
§Features
| Feature | Default | Adds |
|---|---|---|
uring | on | the io_uring backends (Linux); without it, choosing one returns Error::Unsupported |
webpki-roots | off | Mozilla’s root store, tls::webpki_roots (CDLA-Permissive-2.0, so opt-in) |
curl | off | libcurl as an HttpClient implementation (the system libcurl, through pkg-config). It refuses every method but GET, HEAD, OPTIONS and DELETE unless curl_allow_non_idempotent_methods is set: libcurl can send a request twice |
Install with the features you need:
cargo add ergon-io # every backend (uring is on by default)
cargo add ergon-io --features webpki-roots # and Mozilla's root store
cargo add ergon-io --features curl # and libcurl as an HttpClient
cargo add ergon-io --no-default-features # no io_uring backends, no io-uring crate§More
- The book chapter: ergon-io (backends, WebSocket, HTTP client, measured results).
- Terms (busy polling,
io_uring, quick ACK, p50 / p99 / max): glossary. - Every measurement, with hosts and run ids: io/benches/BENCHMARK.md.
- API reference: docs.rs/ergon-io.
Licensed under Apache-2.0.
§Modules
backend:BackendandConfig(Config::parsetakesbackend[,key=value...]).ws_conn:WsConn,WsDial,WsOptions,WsHandler;wsis the frame codec.reconnect:Reconnectand its backoff policy,Retry.http:HttpConn,HttpDial,HttpHandler. On Linux,clientpools connections asHttpClient.tls: the TLS 1.3 client configuration (tls::client_config).auth: request signing (Signer).endpoint:Handler(on_open,on_closed),ConnId, and what aDrivertakes (Endpoint,Dial).checksandroute: the host, NIC and quick-ACK route checks, built on ergon-tune.placement: where the helper threads run (the connect helper, the warning thread,SQPOLL), inside the process’s cpuset.limit: the send budget for venue rate limits (Budget).outcome: what became of a request (Sent,Verdict) andRetry-Afterparsing.net(Target),dns,any(AnyConn: WebSocket and HTTP on one driver),clock,error.
Driver, HttpClient, sys and warnings are Linux-only, so they are
named here without links.
Re-exports§
pub use any::AnyConn;pub use any::AnyDial;pub use auth::SignatureKind;pub use auth::Signer;pub use backend::Backend;pub use backend::Config;pub use client::ClientHandler;pub use client::CurlDrive;pub use client::HttpClient;pub use client::HttpClientConfig;pub use client::HttpImpl;pub use endpoint::ConnId;pub use endpoint::ConnMut;pub use endpoint::Dial;pub use endpoint::DriverStats;pub use endpoint::Endpoint;pub use endpoint::Handler;pub use endpoint::Receive;pub use error::Error;pub use error::Result;pub use http::HttpConn;pub use http::HttpDial;pub use http::HttpHandler;pub use http::HttpOptions;pub use http::HttpStats;pub use http::Response;pub use limit::BucketId;pub use limit::BucketSpec;pub use limit::BucketStats;pub use limit::Budget;pub use limit::Charge;pub use limit::Class;pub use limit::Limited;pub use limit::Resend;pub use limit::Ticket;pub use limit::Why;pub use net::Target;pub use outcome::Now;pub use outcome::Sent;pub use outcome::Verdict;pub use reconnect::Reconnect;pub use reconnect::Retry;pub use ws_conn::WsConn;pub use ws_conn::WsDial;pub use ws_conn::WsHandler;pub use ws_conn::WsOptions;pub use ws_conn::WsStats;
Modules§
- any
- One endpoint type for both protocols, so WebSocket and HTTP connections
share one driver (one
pollper loop iteration). - auth
- Request signing for exchange REST APIs, Binance style: the parameters, a
millisecond
timestamp, and asignatureover exactly those bytes, appended as&signature=…. - backend
- Backend selection: which kernel interface the
Driveruses, chosen at run time from configuration. - checks
- The host and NIC checks a driver runs, built on
ergon_tune. - client
- A pooled HTTP client; the implementation is configuration.
- clock
- Clocks, CPU pinning and per-thread accounting.
- dns
- Non-blocking DNS:
/etc/hosts, a TTL cache, and A-record queries over UDP, advanced byResolver::pollwithout waiting. - endpoint
- What a
Driverdrives: connection ids, the sealed endpoint traits and the application callbacks. Platform-independent. - error
- Error type.
- http
- HTTP/1.1 client: one keep-alive connection.
- limit
- Send budget: venue rate limits checked before a request goes out.
- net
- Connection set-up without blocking: the
Targetdescription and the TCP connect plus TLS handshake every protocol shares. - outcome
- What became of a request: whether it was sent, whether it ran, and the server’s wait.
- placement
- Where ergon-io’s helper threads run:
Driver::connect_background’s dial helper, the warning thread, anduring-sqpoll’sSQPOLLkernel thread. - reconnect
- Reopening connections with backoff:
Reconnect. - route
- The TCP quick-ACK route check.
- sys
- Linux socket options and
recvmsg/sendwithSO_TIMESTAMPINGcontrol messages. - tls
- TLS 1.3: rustls does the handshake; this module then owns the record layer and decrypts each record in place in the receive buffer.
- warnings
- ergon-io’s warning thread (see
checks): waiting for it to catch up. - ws
- RFC 6455 codec: the client handshake, a server handshake (for test and lab servers), frame headers, frame encoding and Close frames.
- ws_conn
- A TLS WebSocket client connection.