Skip to main content

Crate ergon_io

Crate ergon_io 

Source
Expand description

§ergon-io

Crates.io API Docs Book

Network I/O for exchange clients on one busy-spinning thread: WebSocket and HTTP/1.1 over TLS 1.3.

  • One non-blocking poll moves 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-roots

Which features to pick: see Features, below.

§When to use it

Use it forUse 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 kernelProduction on macOS or Windows: the driver is Linux-only
Fixed endpoints: HTTP/1.1 keep-alive, TLS 1.3HTTP/2 or 3, proxies, redirects, cookies, compression: the curl feature or a general client

§Parts

PartWhat it does
DriverOwns the connections. Each poll makes one pass (receive, decrypt, decode, deliver, send) and returns.
WsConnWebSocket client. WsHandler::on_message gets a slice of the receive buffer.
WsOptionsWebSocket limits and timers.
ReconnectOptional. Reopens closed connections with backoff; you resubscribe.
HttpClientA pool of warm HTTP connections behind one API: native (fast) or libcurl (feature curl).
HttpConnOne HTTP/1.1 keep-alive connection.
SignerBinance-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-spin or uring-sqpoll. epoll-busy and uring-napi may wait up to wait_us.
  • One failure closes one connection. It goes to Handler::on_closed; the others keep running. poll fails only when the backend fails.
  • Connecting does not block. Driver::connect takes a step per poll, but the TLS handshake makes one poll long; Driver::connect_background runs 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. Driver and HttpClient are Linux-only; the protocol code builds everywhere.
  • Kernel. uring-spin needs Linux 6.1+; uring-sqpoll, uring-napi and epoll-busy need 6.9+. The io_uring backends need the uring feature (on by default). prefer_busy_poll on recv-busy and a raised busy_poll_budget need CAP_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=off silences those warning lines (require makes a warning refuse the start); the results stay on Driver::host_report().
  • Measured on x86 (AWS c7i, Azure D8ds_v6) and Graviton (AWS c8g): 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-sqpoll with a spare CPU, or recv-busy,per_queue=true on one CPU.
  • The interrupt backends (epoll-spin, uring-spin) were level with uring-sqpoll from 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::Budget checks 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 one outcome::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

FeatureDefaultAdds
uringonthe io_uring backends (Linux); without it, choosing one returns Error::Unsupported
webpki-rootsoffMozilla’s root store, tls::webpki_roots (CDLA-Permissive-2.0, so opt-in)
curlofflibcurl 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

Licensed under Apache-2.0.

§Modules

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 poll per loop iteration).
auth
Request signing for exchange REST APIs, Binance style: the parameters, a millisecond timestamp, and a signature over exactly those bytes, appended as &signature=….
backend
Backend selection: which kernel interface the Driver uses, 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 by Resolver::poll without waiting.
endpoint
What a Driver drives: 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 Target description 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, and uring-sqpoll’s SQPOLL kernel thread.
reconnect
Reopening connections with backoff: Reconnect.
route
The TCP quick-ACK route check.
sys
Linux socket options and recvmsg / send with SO_TIMESTAMPING control 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.

Structs§

Driver
Owns connections of type E and drives them with the configured backend.
RxMeta
Receive metadata for one read.