Skip to main content

Module outgoing_queue

Module outgoing_queue 

Source
Expand description

OutgoingQueue — the outgoing HTTP request queue: per-lane ordering, idempotent retries with capped exponential backoff, and crate::store::StateStore-backed crash safety (plan §5, M12).

§Contract

This module owns none of the flush-epoch machinery itself (that’s crate::persist::FlushEpoch, M1/this piece’s own extension of it) and performs no I/O: OutgoingQueue::releasable hands back plain crate::wire::OutgoingRequest values for a shell to execute; OutgoingQueue::on_response/OutgoingQueue::on_transport_error take the shell’s result back in. now_ms is always an explicit input — this module never calls a clock itself (same rule as crate::ids’s monotonic-sequence inputs).

§Lanes

Every pending request belongs to exactly one Lane, which decides how many of that lane’s requests may be in flight at once:

LaneIn flight at onceWhy
Lane::Sync1 (global)Only one /sync long-poll makes sense at a time.
Lane::ToDevice1 (global)Per-device Olm ratchets are consumed in a strict sequence; two to-device sends racing (including a retry racing the next fresh send) would desynchronize a peer’s ratchet.
Lane::Room(room_id)1 per roomPreserves the room’s own send order (and, for encrypted rooms, its Megolm ratchet order) without serializing unrelated rooms against each other.
Lane::AccountData1 (global), strict FIFOEvery account-data write replaces the WHOLE value under its (room, type) key, so two racing PUTs (or a retry overtaken by a later PUT) would silently lose an update: a later request in this lane never overtakes an earlier one, not even while the earlier one is backing off before a retry.
Lane::Otherup to [OTHER_LANE_CONCURRENCY]Everything with no ordering requirement of its own (profile lookups, user-directory search, /keys/query, …) — bounded so this queue cannot flood a homeserver with unlimited concurrent calls.

OutgoingQueue::releasable always returns requests in the order they were enqueued (see “Restart” below for how that order survives a reload), skipping any whose lane is currently occupied, whose flush epoch is not yet satisfied (crate::persist::FlushEpoch::is_released), or whose retry backoff has not yet elapsed.

§Retry policy

OutgoingQueue::on_response classifies a shell-supplied crate::wire::HttpResponseDescriptor:

  • 2xx → ResponseOutcome::Done — the pending record is deleted.
  • 429 → ResponseOutcome::Retry, using the response body’s own retry_after_ms (Matrix’s M_LIMIT_EXCEEDED shape) if present, falling back to this queue’s own capped exponential backoff otherwise.
  • 5xx, or OutgoingQueue::on_transport_error (no response at all) → ResponseOutcome::Retry with this queue’s own capped exponential backoff: min(60_000, 1000 * 2^attempt) milliseconds, jittered to [50%, 100%] of that value by an injected Jitter source (never a global RNG — this crate keeps no hidden shared mutable state, same rule as crate::ids’s TxnId/ RequestId). attempt counts this request’s own retry attempts and is capped at 6 internally (1000 * 2^6 = 64_000 already exceeds the 60s ceiling), so backoff saturates rather than growing unbounded.
  • Anything else (4xx other than 429, and any other unexpected status) → ResponseOutcome::Failed — the pending record is deleted; the caller (a later piece, sync_engine/MessengerCore) maps this to e.g. SendState::Failed.

A retry always resends the identical crate::wire::OutgoingRequest this queue already has pending — same txnId baked into the path, same body — never re-encrypted, never re-minted. OutgoingQueue does not (and cannot, from this layer) tell a caller to re-encrypt; it simply never discards or mutates a pending request’s own bytes between OutgoingQueue::releasable calls.

§Restart

OutgoingQueue::load rebuilds a queue from crate::store::StateStore::pending_requests, with none marked in flight (a crash mid-flight means the shell doesn’t know whether the previous attempt reached the server) and no retry backoff carried over (backoff state is deliberately ephemeral — see OutgoingQueue’s own fields). Resending after a restart is safe because every mutating Matrix endpoint this crate calls is either server-side idempotent on its own txnId (room sends, redactions, to-device sends) or safely re-uploadable as identical bytes (/keys/upload, per M5’s design) — except /keys/claim, whose resend may consume a different one-time key than the original attempt did; this is an accepted, documented cost (a claimed-but-unused OTK is simply wasted, not a correctness bug) rather than a reason to special-case restart recovery. /sync itself is never persisted as a pending request in the first place — a fresh /sync (using the durable sync_token, a separate crate::store::StateStore record) is always reissued instead of resumed.

OutgoingQueue::load recovers enqueue order from crate::store::StateStore::pending_requests by sorting on crate::ids::RequestId’s own Ord rather than persisting a redundant sequence field: RequestId is documented (ids module doc) to be minted from one caller-owned, strictly increasing counter per process, and its fixed-width-hex encoding keeps that counter’s order exactly lexicographic (ids module’s own txn_id_and_request_id_are_monotonic_and_distinct_namespaces test). This holds as long as every RequestId a MessengerCore mints for its whole lifetime comes from that one counter, never reused or reset — a precondition MessengerCore (a later piece, M13) is responsible for.

§Flush epoch integration

OutgoingQueue::enqueue is the one place this queue touches crate::persist::FlushEpoch: it calls crate::persist::FlushEpoch::seal_for_request before persisting the new pending record, so the returned crate::persist::RequiredSeq only ever covers mutations that predate this request (never the request’s own about-to-be-created pending record — that record’s durability is a separate, ordinary flush the next crate::persist::FlushEpoch::seal_for_request/ crate::persist::FlushGate::take_batch call will pick up). See persist’s module doc for the full per-request epoch rule and a worked timeline.

One consequence worth calling out: crate::store::Store’s FlushEpoch does not distinguish “a real state mutation” from “another request’s own not-yet-batched pending-request record” — both are just dirty crate::persist::RecordKey entries to FlushEpoch::seal_for_request. So enqueuing two requests back to back with no flush/ack in between makes the second one’s required_seq cover the first one’s own bookkeeping write (sealed as a side effect of the second’s own enqueue call). This is benign, not a bug: it self-resolves the moment a shell performs its normal flush/ack tick (which every driving loop — a later piece, sync_engine — does regardless), and it never affects RequiredSeq::NONE requests (a request enqueued with nothing pending at all, e.g. the very first request of a session).

Structs§

OutgoingQueue
The outgoing HTTP request queue — see the module doc.
PendingRequest
One request this queue is tracking: its wire shape, which Lane it belongs to, and the RequiredSeq it must wait on before it may ever be released. Persisted via crate::store::StateStore::save_pending_request so it survives a crash — see the module doc’s “Restart” section for exactly what survives and what doesn’t.

Enums§

Lane
Which ordering class a pending request belongs to — see the module doc’s “Lanes” table for the exact in-flight limit and rationale of each variant.
ResponseOutcome
What happened to a request after a shell fed its result back in via OutgoingQueue::on_response or OutgoingQueue::on_transport_error.

Traits§

Jitter
A source of jitter for retry backoff, injected explicitly rather than reaching for a global RNG — this crate keeps no hidden shared mutable state (same rule as crate::ids’s TxnId/RequestId). Implementors return a value in [0.0, 1.0]; a shell wires in a real RNG, tests wire in a deterministic fake.