Raxol.Agent.Journal behaviour (Raxol Journal v2.8.0)

Copy Markdown View Source

Behaviour for a session's durable append-only journal — the source of truth for the durable tier of events (see the package README, "The journal").

A journal is an ordered, append-only log of events. Each appended event is assigned a monotonic offset (= the event's id) and framed as one complete, self-delimited JSON object per line. Replaying a journal yields the events back in offset order.

The behaviour exists so alternate storage backends (S3, a database, ...) can slot in later behind the same contract. The concrete file-backed implementation is Raxol.Agent.Journal.FileStore — one directory per session, size-capped JSONL segments, torn-tail recovery on replay.

Callbacks

  • open/2 — open (creating if needed) the journal for a session, returning an opaque handle.
  • append/2 — append one event, returning {:ok, offset}.
  • read/2 — replay durable events in offset order.
  • close/2 — release the handle (flush + stop the writer).
  • status/1 — :ok for a healthy journal, {:damaged, offset} if corruption was detected.
  • verify/1 — walk a hash-chained journal: :ok or {:broken, offset}.

Durability & recovery

Only complete records are ever returned. On replay a parse failure on the final line of the last segment is treated as a torn tail (a crash mid-write) and truncated away — everything before it is recovered and status/1 stays :ok. A parse failure anywhere interior marks the session damaged, raises a hard alarm, deletes nothing, and never returns the damaged content downstream.

Hash chain

A backend may offer hash-chained journals (Raxol.Agent.Journal.FileStore does with chain: true): each record links to the hash of the one before it, so changing, removing or truncating any committed record is detected. verify/1 reports the offset of the first record that fails, and status/1 reports the same offset as damage.

Summary

Types

An event to append — any JSON-encodable map.

Opaque, backend-specific handle returned by open/2.

A durable record read back from the journal (a decoded map with an "id").

Monotonic offset assigned to an appended event (= its id).

Stable identifier for a session (also the on-disk directory name).

Callbacks

Append one event. Returns the assigned monotonic offset.

Release the handle (flush pending writes and stop the writer).

Open (creating if needed) the journal for session_id. Returns an opaque handle.

Replay durable events in offset order.

:ok for a healthy journal, {:damaged, offset} naming the first offset where corruption (or, on a chained journal, a chain break) was detected.

Walk the hash chain. :ok when every record links and verifies, {:broken, offset} for the first that does not, {:error, :unchained} for a journal created without a chain.

Functions

Dispatch verify/1 to the backend module that owns handle.

Types

event()

@type event() :: map()

An event to append — any JSON-encodable map.

handle()

@type handle() :: term()

Opaque, backend-specific handle returned by open/2.

journal_record()

@type journal_record() :: map()

A durable record read back from the journal (a decoded map with an "id").

offset()

@type offset() :: non_neg_integer()

Monotonic offset assigned to an appended event (= its id).

session_id()

@type session_id() :: String.t()

Stable identifier for a session (also the on-disk directory name).

Callbacks

append(handle, event)

@callback append(handle(), event()) :: {:ok, offset()} | {:error, term()}

Append one event. Returns the assigned monotonic offset.

close(handle)

@callback close(handle()) :: :ok

Release the handle (flush pending writes and stop the writer).

open(session_id, opts)

@callback open(session_id(), opts :: keyword()) :: {:ok, handle()} | {:error, term()}

Open (creating if needed) the journal for session_id. Returns an opaque handle.

read(handle, opts)

@callback read(handle(), opts :: keyword()) ::
  {:ok, [journal_record()]} | {:error, term()}

Replay durable events in offset order.

Returns {:ok, records} for a healthy (or torn-tail-recovered) journal, or {:error, :damaged} when interior corruption was detected — in which case the damaged content is never returned.

Options:

  • :from_offset — only return records whose id is >= this value.

status(handle)

@callback status(handle()) :: :ok | {:damaged, offset()}

:ok for a healthy journal, {:damaged, offset} naming the first offset where corruption (or, on a chained journal, a chain break) was detected.

verify(handle)

@callback verify(handle()) :: :ok | {:broken, offset()} | {:error, :unchained}

Walk the hash chain. :ok when every record links and verifies, {:broken, offset} for the first that does not, {:error, :unchained} for a journal created without a chain.

Functions

verify(handle)

@spec verify(%{__struct__: module()}) ::
  :ok | {:broken, offset()} | {:error, :unchained}

Dispatch verify/1 to the backend module that owns handle.