Scriba.Circuit (Scriba v0.1.0)

Copy Markdown View Source

Per-projection failure state that has to outlive the producer.

The source's producer dies on purpose to rewind its subscription, so it cannot remember anything across a commit failure. Two decisions need memory it does not have:

  • How long to wait before dying again. Without a delay the transient path crashes as fast as batches form — roughly ten times a second at batch_timeout: 100 — which burns a supervisor's restart budget in seconds regardless of how generous that budget is.

  • Whether repeated single-event integrity failures are one bad row or a systemic mismatch. See record_wipeout/2.

State lives in one shared ETS table for the whole BEAM, keyed by {name, version}, following Scriba.Position's model: created once at application start, one atom regardless of projection count.

Summary

Functions

Records a transient commit failure and returns how long the producer should wait before dying, in milliseconds.

Records a batch in which every attempted write failed on integrity grounds and none committed, and returns whether the projection should halt.

Clears all failure state — called whenever a batch commits.

Functions

record_transient(name, version)

@spec record_transient(String.t(), pos_integer()) :: non_neg_integer()

Records a transient commit failure and returns how long the producer should wait before dying, in milliseconds.

record_wipeout(name, version, attempted)

@spec record_wipeout(String.t(), pos_integer(), pos_integer()) :: :halt | :continue

Records a batch in which every attempted write failed on integrity grounds and none committed, and returns whether the projection should halt.

SQLSTATE says a failure is deterministic and event-specific. It does not say how many events share the defect. A tightened column type or a NOT NULL added to a field the handler never populates makes every insert fail with a class 22/23 code — each one individually dead-letterable, so the per-event fallback would drain the entire stream into scriba_dead_letters, advance the cursor to head, and leave Scriba.info/2 reporting a fully caught-up projection over an empty read model. That is precisely the outcome :structural exists to prevent, reached through a different code class.

Blast radius is the guard that SQLSTATE cannot provide:

  • More than one event attempted and all of them failed → systemic, halt now. One poison row does not take its whole batch with it.
  • Exactly one event attempted → ambiguous, so dead-letter it. If it keeps happening with nothing ever committing (3 batches running), it is systemic after all.

reset(name, version)

@spec reset(String.t(), pos_integer()) :: :ok

Clears all failure state — called whenever a batch commits.