Raxol.AgentClientProtocol.Ext.Journal.Mem (Raxol Agent Client Protocol v0.1.0)

Copy Markdown View Source

ETS-backed in-memory journal — an implementation of Raxol.AgentClientProtocol.Ext.Journal bound by its OFFSET LAW.

Layout

One :ordered_set ETS table per session, keyed by offset ({offset, %Record{}}). Because the table is an :ordered_set keyed on the integer offset, :ets.last/1 is the durable high-watermark in O(log n) and read/3 is an offset-ordered range select.

Ownership (why it survives Writer death)

open/2 creates the table owned by the CALLING process (the app supervisor in production, the test process in tests) — deliberately NOT the Writer. The journal therefore outlives any single Writer crash/restart, which is the precondition for the Writer-restart tip-fold orphan repair.

Protection level (:public, deliberate)

The table is :public, not :protected. :protected would forbid all non-owner writes, but the single-publisher Writer is a DIFFERENT process from the owner (the owner OUTLIVES the Writer, per the ownership note above), so :protected would break every append. Offset integrity is instead structural: exactly one Writer per session (the unique Writer Registry) is the sole appender in production. Records are stored in PLAINTEXT — co-resident BEAM processes are trusted; this store is not a confidentiality boundary.

Atomic offset assignment — no counter

append/2 computes offset = high_watermark(j) + 1 by reading the table max (:ets.last/1), NEVER a cached counter (OFFSET LAW rule 2). Atomicity across concurrent appenders is the Writer's job: a single Writer process per session serializes every append through one mailbox, so the read-then-insert is race-free by construction (the behaviour may assume a single writer per session).

Summary

Functions

Assign the next offset atomically (max + 1, read from the store), stamp session_id + ts_hook, persist, and return the COMPLETE %Record{}.

Delete the backing table. Idempotent-ish: a double close raises like any ETS misuse.

Last DURABLE offset, read from the store (:ets.last/1); 0 for an empty session. NEVER a cached counter (OFFSET LAW rule 2).

Open a fresh per-session handle. The ETS table is owned by the caller (see the ownership note in the moduledoc). opts is accepted for behaviour conformance and ignored.

Offset-ordered records in [from, to]. An empty or inverted range yields {:ok, []}. Reads directly from the store — value-identical to what append stored.

Types

t()

@type t() :: %Raxol.AgentClientProtocol.Ext.Journal.Mem{
  session_id: String.t(),
  table: :ets.tid()
}

Functions

append(j, map)

Assign the next offset atomically (max + 1, read from the store), stamp session_id + ts_hook, persist, and return the COMPLETE %Record{}.

close(mem)

@spec close(t()) :: :ok

Delete the backing table. Idempotent-ish: a double close raises like any ETS misuse.

high_watermark(mem)

@spec high_watermark(t()) :: non_neg_integer()

Last DURABLE offset, read from the store (:ets.last/1); 0 for an empty session. NEVER a cached counter (OFFSET LAW rule 2).

open(session_id, opts \\ [])

@spec open(String.t(), keyword()) :: {:ok, t()}

Open a fresh per-session handle. The ETS table is owned by the caller (see the ownership note in the moduledoc). opts is accepted for behaviour conformance and ignored.

read(j, from, to)

Offset-ordered records in [from, to]. An empty or inverted range yields {:ok, []}. Reads directly from the store — value-identical to what append stored.