Raxol. AgentClientProtocol. Transport. Reassembly
(Raxol Agent Client Protocol v0.1.0)
Copy Markdown
View Source
A pure, process-free reorder buffer for unordered-carrier transports
(pairs with Raxol.AgentClientProtocol.Transport.Envelope on the send
side; no shipped transport uses it).
Shaped exactly like Transport.Framer: no process, no mailbox, no I/O —
a caller (a transport's single reader) threads t() through successive
push/3 calls as enveloped frames arrive off the wire, in whatever
order the carrier delivers them, and gets back the frames that are now
releasable in original send order.
Scope: reliable-unordered, not lossy
This module targets carriers that reorder frames but never lose them
(e.g. frames fanned out over several independently-ordered streams, a
bus with per-partition-only ordering, or a datagram carrier with its
own retransmit below this layer). It restores order; it does not
detect or recover from loss. A missing tseq and a merely-delayed
tseq are indistinguishable to this module — both simply hold up
release until either the gap arrives or the watermark is crossed. A
lossy carrier needs its own ARQ (ack/retransmit) underneath this
layer to turn itself into a reliable-unordered one; that is a transport
concern, not an ordering one, and is out of scope here.
Semantics
next_expectedstarts at1(or:start, seenew/1).tseq == next_expected— release that frame, then cascade: release every already-buffered contiguous successor (next_expected + 1,+2, ...) in onepush/3call, advancingnext_expectedpast all of them.tseq > next_expected— buffer it (keyed bytseq); nothing is released.tseq < next_expected— a duplicate of an already-released frame; dropped silently (idempotent under carrier-level retransmit).- a duplicate of an already-buffered (not yet released)
tseq— also dropped silently; the buffer keeps the first copy. - the buffer crossing either watermark (
:max_buffered_frames, default1024;:max_buffered_bytes, default 16 MiB) returns{:closed, {:transport, :reassembly_overflow}}— the only failure mode. There is no other way this module ever reports a problem.
Overflow-bounded, never timer-bounded
There is no wall clock anywhere in this module — no timer, no
Process.send_after, no deadline. While under the watermark, an
out-of-order frame is held indefinitely, waiting for its missing
predecessor; the only two resolutions are the predecessor's arrival or
the watermark closing the stream. A carrier that wants liveness
detection (peer silently vanished, nothing further will ever arrive)
implements its own keepalive below this layer and calls close/1 (or
simply stops calling push/3 and tears down its own state) — that is
the carrier's reliability concern, not this buffer's ordering concern.
Usage
buf = Reassembly.new()
{:ok, [], buf} = Reassembly.push(buf, 2, %{"b" => 2}) # buffered, nothing releasable yet
{:ok, released, buf} = Reassembly.push(buf, 1, %{"a" => 1})
# released == [%{"a" => 1}, %{"b" => 2}] -- 1 releases, then 2 cascades
Summary
Types
One JSON-RPC frame (a decoded map), as delivered by Envelope.unwrap/1.
The sole failure this module ever produces: the watermark was crossed.
Sender-assigned monotone sequence number (see Envelope).
Functions
Create a fresh reassembly buffer.
Feed one arrived (already-unwrapped) {tseq, frame} pair into the
buffer.
Types
@type frame() :: map()
One JSON-RPC frame (a decoded map), as delivered by Envelope.unwrap/1.
@type overflow_reason() :: {:transport, :reassembly_overflow}
The sole failure this module ever produces: the watermark was crossed.
@type t() :: %Raxol.AgentClientProtocol.Transport.Reassembly{ buffer: %{optional(tseq()) => frame()}, buffered_bytes: non_neg_integer(), max_buffered_bytes: pos_integer(), max_buffered_frames: pos_integer(), next_expected: pos_integer() }
@type tseq() :: pos_integer()
Sender-assigned monotone sequence number (see Envelope).
Functions
Create a fresh reassembly buffer.
Options:
:start— the firsttseqthis buffer expects (default1, matchingEnvelope's counter start). Must be a positive integer.:max_buffered_frames— maximum number of not-yet-releasable frames held at once before overflow (default1024). Must be a positive integer.:max_buffered_bytes— maximum approximate total size (in bytes, via:erlang.external_size/1on each buffered frame) of buffered frames before overflow (default 16777216 = 16 MiB). Must be a positive integer.
Feed one arrived (already-unwrapped) {tseq, frame} pair into the
buffer.
Returns {:ok, released, buffer} where released is the (possibly
empty) list of frames now releasable, in original send order — or
{:closed, {:transport, :reassembly_overflow}} if admitting this frame
would cross either watermark. Once :closed is returned the caller
MUST stop calling push/3 (mirroring T-TERM: nothing is delivered
after a close) and deliver
{:acp_transport, ref, {:closed, {:transport, :reassembly_overflow}}}
to its owner; this module holds no further state for that stream.