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_expected starts at 1 (or :start, see new/1).
  • tseq == next_expected — release that frame, then cascade: release every already-buffered contiguous successor (next_expected + 1, +2, ...) in one push/3 call, advancing next_expected past all of them.
  • tseq > next_expected — buffer it (keyed by tseq); 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, default 1024; :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.

t()

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

frame()

@type frame() :: map()

One JSON-RPC frame (a decoded map), as delivered by Envelope.unwrap/1.

overflow_reason()

@type overflow_reason() :: {:transport, :reassembly_overflow}

The sole failure this module ever produces: the watermark was crossed.

t()

@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()
}

tseq()

@type tseq() :: pos_integer()

Sender-assigned monotone sequence number (see Envelope).

Functions

new(opts \\ [])

@spec new(keyword()) :: t()

Create a fresh reassembly buffer.

Options:

  • :start — the first tseq this buffer expects (default 1, matching Envelope's counter start). Must be a positive integer.
  • :max_buffered_frames — maximum number of not-yet-releasable frames held at once before overflow (default 1024). Must be a positive integer.
  • :max_buffered_bytes — maximum approximate total size (in bytes, via :erlang.external_size/1 on each buffered frame) of buffered frames before overflow (default 16777216 = 16 MiB). Must be a positive integer.

push(t, tseq, frame)

@spec push(t(), tseq(), frame()) ::
  {:ok, [frame()], t()} | {:closed, overflow_reason()}

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.