Replicant.AssemblerServer (Replicant v0.3.1)

Copy Markdown View Source

The serial process shell over the pure Replicant.Assembler (spec §4). Receives decoded pgoutput messages from Replicant.Connection (which decodes behind the value-free boundary and never applies the sink), assembles transactions, and applies the sink synchronously — blocking THIS process, off the Connection's keepalive path. On a durable sink commit (or a watermark skip) it messages the Connection so the ack advances asynchronously; on a fail-closed condition (destructive schema change, sink WRITE fault, an unidentifiable-relation row) it halts the whole pipeline permanently (spec §6/§9).

It is a single serial GenServer (not a Task per transaction) so transactions apply strictly in commit order — the correctness baseline of synchronous per-transaction delivery.

Summary

Functions

Returns a specification to start this module under a supervisor.

Hand a read chunk to the applier. The reply is DEFERRED while max_pending_chunks are buffered. {:error, :window_reset} on a reconnect; {:error, :table_discarded} on a contention discard (drop-cap breach / a PK-less table's concurrent write) — the reader re-reads from durable progress.

A PK-less table's whole-read BARRIER (spec §6.4): after the reader streams all of a keyless table's provisional batches, it calls this to block until those batches have APPLIED (converged past any late contention) — replying :ok — or the table was contention-discarded — replying {:error, :table_discarded} so the reader redoes the whole table. Without this barrier a single-batch keyless read can complete before a late-arriving concurrent write discards its still-pending batch, silently losing the rows.

Open the drop-set tracking window for a table BEFORE the reader captures LW (spec §2 R1). The reply is DEFERRED while the assembler-local in-flight estimate (frontier − last applied LSN) exceeds max_inflight_lag ÷ 2 — the pacing gate (spec §4 R2): stream drain gets priority, chunks fill idle capacity.

The Registry via-name a pipeline's AssemblerServer registers under.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

deliver_snapshot_chunk(server, chunk)

@spec deliver_snapshot_chunk(GenServer.server(), Replicant.SnapshotWindow.chunk()) ::
  :ok | {:error, :window_reset} | {:error, :table_discarded}

Hand a read chunk to the applier. The reply is DEFERRED while max_pending_chunks are buffered. {:error, :window_reset} on a reconnect; {:error, :table_discarded} on a contention discard (drop-cap breach / a PK-less table's concurrent write) — the reader re-reads from durable progress.

finish_snapshot_table(server, qualified, epoch)

@spec finish_snapshot_table(GenServer.server(), String.t(), non_neg_integer()) ::
  :ok | {:error, :window_reset} | {:error, :table_discarded}

A PK-less table's whole-read BARRIER (spec §6.4): after the reader streams all of a keyless table's provisional batches, it calls this to block until those batches have APPLIED (converged past any late contention) — replying :ok — or the table was contention-discarded — replying {:error, :table_discarded} so the reader redoes the whole table. Without this barrier a single-batch keyless read can complete before a late-arriving concurrent write discards its still-pending batch, silently losing the rows.

epoch is the window GENERATION the reader captured at open_snapshot_window/2. If a reconnect reset re-seated the window (bumped the epoch) since the reader opened this table, its provisional batches were WIPED (not applied): the barrier MUST reply {:error, :window_reset} — never a spurious :ok (which would mark a never-delivered table done → data loss). The stale-generation check comes FIRST because a reset also clears discarded/pending, so a wiped table otherwise reads as clean. {:error, :window_reset} also fires when a reconnect releases a deferred barrier.

open_snapshot_window(server, qualified)

@spec open_snapshot_window(GenServer.server(), String.t()) ::
  {:ok, non_neg_integer()} | {:error, :window_reset | :table_discarded}

Open the drop-set tracking window for a table BEFORE the reader captures LW (spec §2 R1). The reply is DEFERRED while the assembler-local in-flight estimate (frontier − last applied LSN) exceeds max_inflight_lag ÷ 2 — the pacing gate (spec §4 R2): stream drain gets priority, chunks fill idle capacity.

Replies {:ok, epoch} — the window GENERATION the reader now operates under. The keyless reader threads this back to finish_snapshot_table/3 so the barrier can reject a stale generation (a reconnect reset bumped the epoch since the reader opened → its batches were WIPED, not applied — spec §4/§6.4, the 85672f1 sender-tags-epoch discipline). Replies {:error, :table_discarded} if the table was contention-discarded (reader re-reads); {:error, :window_reset} if a reconnect released the deferred open.

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

via(slot_name)

@spec via(String.t()) :: {:via, module(), term()}

The Registry via-name a pipeline's AssemblerServer registers under.