Replicant.SnapshotWindow (Replicant v0.3.1)

Copy Markdown View Source

Pure window algebra for the incremental backfill (spec §2/§4): per-table PK tracking opened BEFORE the reader captures LW (superset drop-set), pending chunks that become ready when the epoch-guarded frontier passes their HW, the drop-filter at apply, the derived drop-cap (discard + re-read on breach), and the pending-chunk capacity bound.

Value-free discipline (Critical Rule 1): this module holds PK VALUES (tracking sets, chunk rows) as data-plane state; nothing here formats, logs, or raises them — every public return is a tagged tuple over the caller's own data.

Summary

Functions

Buffer a chunk awaiting closure. :at_capacity when max_pending is reached (caller defers the reader).

Clear a table's discard flag once its reader has been told to re-read.

True when qualified's pending chunks were discarded (contention) and its reader not yet told.

Advance the frontier from an applied/skipped txn commit LSN (same-process, always current epoch).

Start tracking a table's flushed PKs. MUST precede the reader's LW capture (spec §2 R1).

Pop the first CLOSED pending chunk (frontier >= hw), drop-filtered. Chunks pop in delivery order; a not-yet-closed head blocks later chunks of the same table (PK order must be preserved per table) — and since the reader is serial, pending chunks are already globally ordered.

Reconnect re-seed (spec §4): discard ALL pending chunks + tracking, adopt the new epoch.

Advance the frontier from an epoch-tagged wal_end (stale epochs ignored — 85672f1 class).

Any of qualified's chunks still buffered (pending, not yet applied)?

Conservatively DISCARD a table's pending chunks, RESET its tracking entry, AND SIGNAL RE-READ (spec §2/§4/§5/§6.4). Used by the applier when a delivered transaction's changes is a lazy, single-pass spill-backed Enumerable (or is otherwise unavailable) and so MUST NOT be enumerated to update the drop-set: the table's in-flight chunks are dropped and the reader re-reads from durable progress.

Record delivered txn changes' PKs for every OPEN table (receipt-time, convergence-safe).

track/2 + contention enforcement: a table whose pending chunks must be DISCARDED and re-read is returned in the discard list AND recorded in w.discarded so the caller can signal its reader {:error, :table_discarded} (spec §4/§6.4). Two triggers

True when the PK tuple is in the table's tracking set (test/inspection helper).

Types

chunk()

@type chunk() :: %{
  qualified: String.t(),
  schema: String.t(),
  table: String.t(),
  pk_raw: [String.t()],
  pk_canon: [[term()]],
  changes: [Replicant.Change.t()],
  hw: Replicant.lsn(),
  first?: boolean(),
  complete?: boolean(),
  progress: binary(),
  bound: [term()] | nil
}

t()

@type t() :: %Replicant.SnapshotWindow{
  discarded: %{optional(String.t()) => true},
  drop_cap: pos_integer(),
  epoch: non_neg_integer(),
  frontier: Replicant.lsn(),
  max_pending: pos_integer(),
  pending: [chunk()],
  tracking: %{optional(String.t()) => %{pks: MapSet.t(), pk_raw: [String.t()]}}
}

Functions

add_chunk(w, chunk)

@spec add_chunk(t(), chunk()) :: {t(), :ok | :at_capacity}

Buffer a chunk awaiting closure. :at_capacity when max_pending is reached (caller defers the reader).

clear_discarded(w, qualified)

@spec clear_discarded(t(), String.t()) :: t()

Clear a table's discard flag once its reader has been told to re-read.

discarded?(snapshot_window, qualified)

@spec discarded?(t(), String.t()) :: boolean()

True when qualified's pending chunks were discarded (contention) and its reader not yet told.

new(opts)

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

observe_applied(w, lsn)

@spec observe_applied(t(), Replicant.lsn()) :: t()

Advance the frontier from an applied/skipped txn commit LSN (same-process, always current epoch).

open_window(w, qualified)

@spec open_window(t(), String.t()) :: t()

Start tracking a table's flushed PKs. MUST precede the reader's LW capture (spec §2 R1).

pop_ready(w)

@spec pop_ready(t()) ::
  {:apply, [Replicant.Change.t()], chunk(), t()}
  | {:discard, chunk(), t()}
  | :none

Pop the first CLOSED pending chunk (frontier >= hw), drop-filtered. Chunks pop in delivery order; a not-yet-closed head blocks later chunks of the same table (PK order must be preserved per table) — and since the reader is serial, pending chunks are already globally ordered.

reset(w, new_epoch)

@spec reset(t(), non_neg_integer()) :: t()

Reconnect re-seed (spec §4): discard ALL pending chunks + tracking, adopt the new epoch.

set_frontier(w, e, lsn)

@spec set_frontier(t(), non_neg_integer(), Replicant.lsn()) :: t()

Advance the frontier from an epoch-tagged wal_end (stale epochs ignored — 85672f1 class).

table_pending?(snapshot_window, qualified)

@spec table_pending?(t(), String.t()) :: boolean()

Any of qualified's chunks still buffered (pending, not yet applied)?

taint_table(w, qualified)

@spec taint_table(t(), String.t()) :: t()

Conservatively DISCARD a table's pending chunks, RESET its tracking entry, AND SIGNAL RE-READ (spec §2/§4/§5/§6.4). Used by the applier when a delivered transaction's changes is a lazy, single-pass spill-backed Enumerable (or is otherwise unavailable) and so MUST NOT be enumerated to update the drop-set: the table's in-flight chunks are dropped and the reader re-reads from durable progress.

The tainted table is folded into w.discarded — the SAME reader-facing signal the keyed drop-cap breach / keyless concurrent-write taint use (apply_contention/3) — so the reader's next open/deliver/barrier for it returns {:error, :table_discarded} and re-reads. Without this signal the reader would never learn its chunks were dropped: its bound would advance, a later chunk would persist a bound PAST the discarded chunks, and the discarded chunks' untouched rows would be LOST (the confirmed data-loss hole). Convergence-safe (discard-and-re-read — never data loss, never a chunk whose drop-set is now unknowable). A no-op for a table that is not being tracked (no chunks to drop, nothing to re-read).

track(w, changes)

@spec track(t(), [Replicant.Change.t()]) :: t()

Record delivered txn changes' PKs for every OPEN table (receipt-time, convergence-safe).

track_capped(w, changes)

@spec track_capped(t(), [Replicant.Change.t()]) :: {t(), [String.t()]}

track/2 + contention enforcement: a table whose pending chunks must be DISCARDED and re-read is returned in the discard list AND recorded in w.discarded so the caller can signal its reader {:error, :table_discarded} (spec §4/§6.4). Two triggers:

  • a KEYED table whose tracking set breaches drop_cap (superset discard + re-read); and
  • a PK-LESS table (its tracking entry's pk_raw == []) that received ANY tracked write — drop_cap can never fire for a keyless table (its PK tuple collapses to []), so any concurrent write potentially staled the whole read ⇒ redo (spec §6.4).

tracked?(w, qualified, pk_tuple)

@spec tracked?(t(), String.t(), [term()]) :: boolean()

True when the PK tuple is in the table's tracking set (test/inspection helper).