The durable snapshot-progress persistence contract (C2), the sibling of
Capstan.CheckpointStore.
A snapshot store persists exactly one value per pipeline identity: the whole
Capstan.Snapshot.State (Pinned decision #3) — the phase, the pipeline-global floor p0,
and the per-{schema, table} progress (fingerprint, PK shape, cursor, done flag). This
mirrors CheckpointStore's one-durable-value model, so a caller advancing the backfill
never hand-rolls the persistence and the two stores stay structurally parallel.
The snapshot state is orthogonal auxiliary state, never a replication position: it
carries no GTID that could become a Capstan.Gtid.member?/2 dedup floor (ADR-0001). The
processed-GTID checkpoint stays the sole authoritative replication position; this store only
records "how far the backfill has read".
Behaviour
read/1—{:ok, %Capstan.Snapshot.State{} | nil}(nil= never written) or a value-free{:error, term()}.write/2— durably store the%State{}(idempotent: writing the same value twice is indistinguishable from writing it once) or{:error, term()}.
A read/write fault propagates through read/2/write/3 — it is never collapsed to
{:ok, nil}. A store that faulted must fail closed: masquerading as "never snapshotted"
would make a re-bootstrap re-scan the whole backfill from zero (a silent duplicate).
Callers budget the fault with the SHARED Capstan.CheckpointStore.retry_decision/2 +
permanent_reason?/1 (this store does NOT re-derive the counter), then halt fail-closed
(:snapshot_state_read_failed / :snapshot_state_write_failed).
Rule 1 (Ch5) — binding on every implementation
The %State{} passed to write/2 holds USER DATA: each table's pk_cursor is a row
value and its fingerprint is a column-derived hash. An implementation must never log,
telemeter, or otherwise emit the write/2 argument (nor the value returned by read/1).
Persist it to the durable store and nowhere else. %Capstan.Snapshot.State's derived
Inspect elides the per-table map, but a store that stringifies or logs the struct through
another path would defeat that — so this binding is on the STORE, not only the struct.
Capstan.SnapshotStore.InMemory is a process-lifetime reference implementation for tests
and ephemeral pipelines — it is NOT durable across a restart, so it must never be used
as a production snapshot store.
Summary
Callbacks
Read the durable snapshot state for this pipeline: {:ok, %State{} | nil} (nil = never
written) or a value-free error.
Durably persist the whole %State{} for this pipeline. Idempotent — re-writing the same
value is a no-op-equivalent. Returns :ok or a value-free error. Rule 1: never log or
telemeter the argument.
Functions
Read the snapshot state through impl, propagating a fault fail-closed.
Persist the snapshot state through impl. impl is the callback module; store its handle.
Types
@type store() :: term()
A per-pipeline store handle (e.g. the pid/name of a store process).
Callbacks
@callback read(store()) :: {:ok, Capstan.Snapshot.State.t() | nil} | {:error, term()}
Read the durable snapshot state for this pipeline: {:ok, %State{} | nil} (nil = never
written) or a value-free error.
@callback write(store(), Capstan.Snapshot.State.t()) :: :ok | {:error, term()}
Durably persist the whole %State{} for this pipeline. Idempotent — re-writing the same
value is a no-op-equivalent. Returns :ok or a value-free error. Rule 1: never log or
telemeter the argument.
Functions
@spec read(module(), store()) :: {:ok, Capstan.Snapshot.State.t() | nil} | {:error, term()}
Read the snapshot state through impl, propagating a fault fail-closed.
Never collapses {:error, _} to {:ok, nil} — a faulted store must not read as
"never snapshotted". impl is the callback module; store its handle.
@spec write(module(), store(), Capstan.Snapshot.State.t()) :: :ok | {:error, term()}
Persist the snapshot state through impl. impl is the callback module; store its handle.