Scriba.DeadLetter (Scriba v0.2.2)

Copy Markdown View Source

Helpers for the scriba_dead_letters table — failed events recorded for later inspection or replay.

An event arrives here in five ways (§9). Two are handler failures, after retry exhaustion:

  1. The handler returned {:error, reason}.
  2. The handler raised — the engine catches it and tags it {:exception, exception, stacktrace}.

Three more bypass the retry layer entirely, because retrying them changes nothing:

  1. The commit failed with an integrity-class error, isolated by the per-event fallback pass ({:commit_error, reason}).
  2. The handler returned a {:multi, _} whose operation names collide with another event's in the same batch ({:multi_key_collision, keys}).
  3. The handler returned a shape the target cannot apply.

All of them end up routed to a row in scriba_dead_letters with a serialized copy of the event and an error description. Per §9.2, the projection's position advances past the dead-lettered event — the projection does not block. There is no replay function yet; replaying means reading the row and re-dispatching the event yourself.

Transient and structural commit failures are not dead-letter paths: a transient one replays the whole batch, a structural one halts the projection. See Scriba.Target.

This module exposes raw helpers; the routing decision (when to insert) lives in the Pipeline, which partitions failure-shape handler results out of the batch and passes them to the target as dead_letters.

Summary

Functions

Builds a row map for the scriba_dead_letters table from a failing event and an error tuple/exception.

Counts dead letters for a projection. Takes the same filters as list/3.

Inserts a single dead-letter row directly via repo. Used by callers that manage their own transaction (e.g. retry-then-dead-letter flows).

Lists dead letters for a projection, newest first.

Appends a dead-letter insert to an Ecto.Multi. Useful for the Pipeline's "dead-letter and advance position" transaction after a target apply_batch has already failed and rolled back.

A summary an operator can page on: how many, of what kind, over what span.

Types

error()

@type error() :: %{
  kind: String.t(),
  message: String.t() | nil,
  stacktrace: String.t() | nil
}

projection()

@type projection() :: %{name: String.t(), version: pos_integer()}

Functions

build_row(map, event, error)

@spec build_row(projection(), Scriba.Event.t(), term()) :: map()

Builds a row map for the scriba_dead_letters table from a failing event and an error tuple/exception.

Accepts:

  • {:error, reason} — kind = "error", message = inspect(reason)
  • {:exception, exception, stacktrace} — kind is the exception module as a string, message + formatted stacktrace
  • {:commit_error, reason} — kind = "commit:<SQLSTATE label>"
  • {:multi_key_collision, keys} — kind = "multi_key_collision"
  • anything else — kind = "invalid_return"

count(repo, map, opts \\ [])

@spec count(module(), projection(), keyword()) :: non_neg_integer()

Counts dead letters for a projection. Takes the same filters as list/3.

insert(repo, projection, event, error)

@spec insert(module(), projection(), Scriba.Event.t(), term()) ::
  {:ok, term()} | {:error, term()}

Inserts a single dead-letter row directly via repo. Used by callers that manage their own transaction (e.g. retry-then-dead-letter flows).

list(repo, map, opts \\ [])

@spec list(module(), projection(), keyword()) :: [map()]

Lists dead letters for a projection, newest first.

Returns maps with :id, :position, :stream_id, :event_type, :error_kind, :error_message, :occurred_at and :event_data.

Options

  • :limit (default 50) — how many rows.
  • :offset (default 0) — for paging.
  • :stream_id — only this stream.
  • :error_kind — only this kind, e.g. "commit:23505 (unique_violation)" or "Elixir.ArgumentError". Matches exactly.
  • :since — only rows at or after this DateTime.
  • :order — :desc (default, newest first) or :asc (oldest first, which is the order you would replay in).

:event_data comes back as the map that was stored, not the original struct: build_row/3 serializes __struct__ to a string, so a row records what failed rather than a value you can re-dispatch. Replay reads the event from the source by :position.

multi(multi, projection, event, error)

@spec multi(Ecto.Multi.t(), projection(), Scriba.Event.t(), term()) :: Ecto.Multi.t()

Appends a dead-letter insert to an Ecto.Multi. Useful for the Pipeline's "dead-letter and advance position" transaction after a target apply_batch has already failed and rolled back.

stats(repo, map, opts \\ [])

@spec stats(module(), projection(), keyword()) :: map()

A summary an operator can page on: how many, of what kind, over what span.

%{
  total: 143,
  by_error_kind: %{"commit:23505 (unique_violation)" => 140, "Elixir.ArgumentError" => 3},
  oldest: ~U[...],
  newest: ~U[...]
}

The shape of by_error_kind is the diagnosis. One kind dominating a single stream is a poison event; one kind spread across every stream is a schema or handler problem that dead-lettering is papering over — the case Scriba.Circuit halts on when it catches a whole batch at once.