Malachi.Storage.ElixirStore (malachi v0.8.13)

View Source

Pure-Elixir Malachi.Storage.SegmentStore implementation.

File-per-segment, append-only, with batched writes and an fsync-before-ack durability contract. append/2 buffers; the buffer is flushed and fsynced on an explicit sync/1 or automatically once it reaches :flush_bytes (default 10MB) or :flush_count records (default 20k): NorthGuard's size and count triggers. Maintains an in-memory sparse index ({offset, file_position} every :index_interval bytes) for seeking, kept in an :array sorted by offset so a lookup is an O(log n) binary search; the index is persisted to a sidecar on seal/1 and rebuilt by scanning on recover/3.

This is deliberately a plain module operating on an immutable handle (no GenServer), so it is deterministic and trivial to property-test. The time-based flush trigger (~10ms) and concurrency belong in a higher layer (Malachi.BrokerServer) built on top of this.

Reads via :file.pread/3 and writes via :file.pwrite/3 use explicit positions, so the single file descriptor serves both append and random read without position races. Recovery scans the segment in bounded chunks, so it never loads the whole file at once.

Summary

Types

One sparse-index entry: a logical offset and the byte position where it starts.

What the last scan of this segment concluded: :ok, or the first damage it hit, with the byte position and how much of the file could not be read. Only recover/3 scans, so a freshly opened or read-only handle is :ok by construction.

One buffered, not-yet-flushed record: its offset, encoded frame, and frame size.

t()

Types

index_entry()

@type index_entry() ::
  {offset :: non_neg_integer(), file_position :: non_neg_integer()}

One sparse-index entry: a logical offset and the byte position where it starts.

integrity_verdict()

@type integrity_verdict() :: %{
  reason: atom(),
  position: non_neg_integer(),
  unreadable_bytes: non_neg_integer(),
  sealed?: boolean()
}

What the last scan of this segment concluded: :ok, or the first damage it hit, with the byte position and how much of the file could not be read. Only recover/3 scans, so a freshly opened or read-only handle is :ok by construction.

pending_frame()

@type pending_frame() ::
  {offset :: non_neg_integer(), frame :: iodata(), frame_size :: pos_integer()}

One buffered, not-yet-flushed record: its offset, encoded frame, and frame size.

t()

@type t() :: %Malachi.Storage.ElixirStore{
  file_descriptor: :file.fd(),
  flush_bytes: pos_integer(),
  flush_count: pos_integer(),
  index: :array.array(),
  index_interval: pos_integer(),
  integrity: :ok | integrity_verdict(),
  last_indexed_position: integer(),
  next_offset: non_neg_integer(),
  pending: [pending_frame()],
  pending_bytes: non_neg_integer(),
  pending_count: non_neg_integer(),
  segment: Malachi.Log.Segment.t(),
  write_position: non_neg_integer()
}