Skip to main content

Design

Where ruststream-zeromq sits: between the RustStream runtime and the zeromq client, with a socket per subscription and per publisher, and no server.

The service, the RustStream runtime, this crate, the zeromq client and the peers, top to bottomYour serviceRustStream coreruststream-zeromqzeromq client#[subscriber]handlersRustStream::new(..).with_broker(..)Runtime: dispatch, codecs, retries,batches assembled on the clientSubscription: a taskreads the socket intoa bounded channelPublisher: a socketbind: at startupdial: first publishZmqQueue, ZmqFanout,ZmqRpcShared state: theendpoint, the boundlistener, closedFrame layoutname, headers, payloadPULL, SUB, ROUTERPUSH, PUB, XPUB,ROUTER, DEALERPeers: other processes, any language,no server between themUnder the testing feature, an in-process transportcan take the place of the sockets
The diagram as text
  1. Your service: #[subscriber] handlers; RustStream::new(..) .with_broker(..)
  2. RustStream core: Runtime: dispatch, codecs, retries, batches assembled on the client
  3. ruststream-zeromq: Subscription: a task reads the socket into a bounded channel; Publisher: a socket bind: at startup dial: first publish; ZmqQueue, ZmqFanout, ZmqRpc; Shared state: the endpoint, the bound listener, closed; Frame layout name, headers, payload
  4. zeromq client: PULL, SUB, ROUTER; PUSH, PUB, XPUB, ROUTER, DEALER
  5. #[subscriber] handlers → Runtime: dispatch, codecs, retries, batches assembled on the client: mounted
  6. Runtime: dispatch, codecs, retries, batches assembled on the client → Subscription: a task reads the socket into a bounded channel: polls
  7. Subscription: a task reads the socket into a bounded channel → PULL, SUB, ROUTER
  8. Publisher: a socket bind: at startup dial: first publish → PUSH, PUB, XPUB, ROUTER, DEALER
  9. PULL, SUB, ROUTER → Peers: other processes, any language, no server between them
  10. PUSH, PUB, XPUB, ROUTER, DEALER → Peers: other processes, any language, no server between them
  11. Under the testing feature, an in-process transport can take the place of the sockets

The crate implements the core's broker traits over zeromq, a ZeroMQ client written in Rust. The runtime above it is the same for every broker. Everything ZeroMQ-specific is in this crate: three brokers, one per socket pattern, the frame layout, what an ack means and where a retry copy goes.

Each broker carries the side of its endpoint as a type, Bind or Connect. The side decides whether a subscription can address its own retry copies, and the type makes that a compile-time fact. ZmqRpc has no side parameter: its copies always go where the mount names.

A subscription is a name, the broker carries read_ahead, and the endpoint is the rest of the configuration. A publish carries its frames alone, because a ZMTP send takes frames and nothing else.

Each subscription opens a socket of its own and a task that owns it. The task reads one message at a time into a channel the handler takes from, as Backpressure draws. Each publisher has a socket of its own: on the bind side it attaches at startup, so peers that dial it connect before the first message, and a dialing publisher attaches on its first publish. A publisher on an endpoint this service binds for one of its subscriptions dials that subscription's listener. On the fan-out it is an XPUB that waits for the subscription's filter once, at the attach, so its first message is not dropped.

Every socket attaches, and every task runs, on the runtime the broker connected on, whichever runtime asked. A handler on a thread of its own can publish, and the socket it attaches outlives that thread's runtime.

A queue send survives a dropped publish. The client takes the peer out of its rotation while a send to it waits, so a send dropped at that moment would lose the peer for good. The publisher keeps the send in flight beside its socket, and the next publish completes it before its own. The crate forbids unsafe code, so the selfie and aliasable crates hold the send that borrows its socket. The socket is boxed once per publisher, and a publish adds no allocation to what the client spends.

In process​

The testing feature adds a second transport, which TestApp connects instead of the sockets. Each broker keeps its connected form, its bookkeeping and its publish policies; only the socket underneath changes. A publish is framed the way a socket publisher frames it, and a subscription reads the frames the way its socket task reads them.

Who receives a message is the pattern's rule. A bound queue subscription takes everything pushed to it, and queue subscriptions that dial take turns. A fan-out message reaches every subscription whose name is a prefix of it. A request reaches a responder behind a peer identity, and the answer goes back to the request that asked.

The refusals the sockets make hold in process too, in the same words. Settlement answers Unsupported there as well.

The in-process transport tests the service's logic, not the transport. How far a subscription reads ahead, back-pressure and the real handshake run over loopback sockets, in the live mode of the same test. A production build carries none of it: the crate checks the size of a delivery and of every handle at compile time.