protean eventstore dlq
The protean eventstore dlq command group manages the event store's
dead-letter queue: the positions a subscription retried up to max_retries
and then gave up on. Those give-ups are recorded as Exhausted records in
each subscription's internal failed-* stream. list enumerates the
exhausted positions per subscription; inspect re-reads the failing event so
you can see what could not be processed; replay re-drives one position
through its handler; purge clears one with a terminal Purged marker.
This is different from protean dlq. protean dlq manages the
broker dead-letter queue (messages a broker subscription failed and moved
to {stream_category}:dlq). protean eventstore dlq covers event-store
subscriptions (event handlers, command handlers, projectors, process managers),
which do not use a broker DLQ; they track failed positions and, on exhaustion,
leave the event in place in the store.
All commands accept a --domain option for the domain module path (defaults to
the current directory). list and inspect also accept a --json flag for the
shared CLI result envelope; replay and purge have no JSON output.
Commands
| Command | Description |
|---|---|
protean eventstore dlq list |
List exhausted positions, grouped by subscription |
protean eventstore dlq inspect |
Re-read the failing event behind an exhausted position |
protean eventstore dlq replay |
Re-drive an exhausted position through its handler |
protean eventstore dlq purge |
Clear an exhausted position with a terminal Purged marker |
protean eventstore dlq list
Lists the exhausted positions across all event-store subscriptions, or a single one filtered by stream category.
# All subscriptions
protean eventstore dlq list --domain=my_domain
# One subscription (by stream category)
protean eventstore dlq list --subscription=order --domain=my_domain
# Machine-readable JSON
protean eventstore dlq list --domain=my_domain --json
Options
| Option | Description | Default |
|---|---|---|
--domain |
Domain module path | . (current directory) |
--subscription |
Filter by stream category (e.g. order) |
All subscriptions |
--json |
Output the result envelope instead of a table | False |
Output
Exhausted positions — my_domain
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ Handler ┃ Stream Category ┃ Exhausted Positions ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ app.handlers.OrderHandler │ order │ 42, 87 │
└───────────────────────────────┴─────────────────┴─────────────────────┘
2 exhausted position(s) across 1 subscription(s).
A subscription with no exhausted positions is not shown. When nothing is
exhausted anywhere, the command prints No exhausted positions. and exits 0.
Under --json the result is the shared result envelope, with the
per-subscription list under data.subscriptions:
{
"version": "0.1.0",
"status": "pass",
"data": {
"subscriptions": [
{
"handler": "app.handlers.OrderHandler",
"stream_category": "order",
"exhausted": [42, 87]
}
]
},
"diagnostics": []
}
protean eventstore dlq inspect
Re-reads the event behind an exhausted position and prints its type, global position, and data. The event is read live from the store (it is never copied into the exhausted record), so you always see its current form.
protean eventstore dlq inspect 42 --domain=my_domain
# Machine-readable JSON
protean eventstore dlq inspect 42 --domain=my_domain --json
Options
| Option | Description | Default |
|---|---|---|
POSITION |
Exhausted global position (positional argument) | Required |
--domain |
Domain module path | . (current directory) |
--subscription |
Stream category to search in | All subscriptions |
--json |
Output the result envelope instead of text | False |
Under --json the envelope's data carries position, type,
global_position, and data:
{
"version": "0.1.0",
"status": "pass",
"data": {
"position": 42,
"type": "MyDomain.OrderPlaced.v1",
"global_position": 42,
"data": { "order_id": "abc", "total": 100 }
},
"diagnostics": []
}
protean eventstore dlq replay
Re-drives one exhausted position through its handler. It reads the failing
event and dispatches it to the handler exactly once. On success it records a
resolution, the position stops being listed as exhausted, and the command exits
0; on a repeat failure it reopens the position and exits 1 (the handler's
reason is in the engine logs). A subscription rebuilds its failed positions when
it starts, so a server that is already running retries a reopened position after
its next restart. The subscription read cursor is never moved, so replaying one
position does not re-process every event since the failure.
protean eventstore dlq replay 42 --domain=my_domain
# Pick one handler when a position is exhausted in more than one on the same stream
protean eventstore dlq replay 42 --handler=app.handlers.OrderHandler --domain=my_domain
# Skip the confirmation prompt (for scripts)
protean eventstore dlq replay 42 --domain=my_domain --yes
Options
| Option | Description | Default |
|---|---|---|
POSITION |
Exhausted global position (positional argument) | Required |
--domain |
Domain module path | . (current directory) |
--subscription |
Stream category to search in | All subscriptions |
--handler |
Handler (its fqn or class name, as list prints it) when a position is exhausted in more than one |
All handlers |
--yes, -y |
Skip the confirmation prompt | False |
Replay re-runs handler side effects, so it confirms first. Every replay can
apply its side effects again: it dispatches out-of-band and never consults the
idempotency store, and an exhausted command never recorded a success to
deduplicate against. The prompt names the target (an event handler, a projector,
a process manager, or a command with or without an idempotency key) so the
operator knows what is being re-run. Replaying a projector position re-applies
its projection writes, and replaying a process manager position can issue its
commands again. An idempotency key helps only when the handler itself uses it to
stay idempotent.
A command whose deadline has passed is refused, because the engine would skip an
expired command; purge it instead. The deadline is checked again right before the
dispatch, so a command whose deadline ran out while the prompt was open is
refused too. EventStoreSubscription.replay_exhausted, the method that
dispatches, checks the deadline once more and refuses with the same error. The
engine then checks it a last time with a later clock reading, so a deadline that
runs out between those two checks is still skipped and the position recorded
resolved. A command whose handler is no longer registered is refused as well:
the dispatcher would find nothing to route it to, and the replay would report the
position resolved without running anything. A handler-level idempotency
declaration that would let replay refuse a non-idempotent target does not exist
yet.
Both replay and purge re-read the position's latest status right before they
act. If another operator's replay or purge cleared it while the prompt was
open, the command refuses and does nothing.
protean eventstore dlq purge
Clears one exhausted position. The failed-* streams are append-only, so purge
keeps the record history and writes a new Purged record after the Exhausted
one. The position stops being listed as exhausted and a later engine restart
does not re-track it. Purge does not re-run the handler, so it needs no engine.
Like replay, it refuses a position that stopped being exhausted while the
prompt was open.
protean eventstore dlq purge 42 --domain=my_domain
# Skip the confirmation prompt
protean eventstore dlq purge 42 --domain=my_domain --yes
Options
| Option | Description | Default |
|---|---|---|
POSITION |
Exhausted global position (positional argument) | Required |
--domain |
Domain module path | . (current directory) |
--subscription |
Stream category to search in | All subscriptions |
--handler |
Handler (its fqn or class name, as list prints it) when a position is exhausted in more than one |
All handlers |
--yes, -y |
Skip the confirmation prompt | False |
Exit codes
| Code | Meaning |
|---|---|
0 |
Success, including "no exhausted positions", a replay that resolves, and a purge. |
1 |
Human mode (--json not set): the domain failed to load; replay/purge was not confirmed (Typer aborts, the same as every other protean command); or a replay that reopened the position (the handler failed again). |
2 |
Usage or environment error: unknown --subscription or --handler, unknown position, an event that can no longer be re-read, an expired command, a command with no registered handler, a position another command cleared while the prompt was open, or a position exhausted in more than one subscription with no --handler to choose one. Under --json, a domain that failed to load also exits 2 and emits the error envelope. |
Error handling
| Condition | Behavior |
|---|---|
| Invalid domain path | Human mode: "Error loading Protean domain", Typer aborts with exit 1. Under --json: error envelope, exit 2. |
--subscription matches no event-store subscription |
"No event-store subscription found for stream category ...", exit 2 |
--handler matches no event-store subscription |
"No event-store subscription found for handler ...", exit 2 |
inspect/replay/purge position is not exhausted |
"No exhausted position ... found", exit 2 |
inspect/replay event can no longer be read |
"Could not re-read the event ...", exit 2 |
replay targets a command whose deadline has passed |
"Position ... targets a command whose deadline has passed ...", exit 2 |
replay targets a command with no registered handler |
"The command dispatcher has no handler for the message at position ...", exit 2 |
replay/purge position is exhausted in more than one subscription |
"Position ... is exhausted in multiple subscriptions ...", exit 2 |
replay/purge position was cleared while the prompt was open |
"Position ... is no longer exhausted ...", exit 2 |
replay/purge confirmation declined |
Typer aborts with exit 1 |
replay handler fails again |
"... position reopened. A running server retries it after its next restart.", exit 1 |
How positions get exhausted
When an event-store handler raises, the subscription records the failed
position and retries it on each recovery pass. After max_retries retries it
writes an Exhausted record and stops retrying. The record carries the failing
event's stream_name and stream_position so inspect can locate the event;
records written before this was added fall back to the origin stream, read by
global position. Either read is checked against the record's global position,
which names the message store-wide. Store reads are inclusive, so if the message
a record names is gone, the read comes back with the next one in the stream, and
a restore that dropped a stream's tail can leave a later append sitting at the
per-stream ordinal the record names. inspect and replay treat either as an
event they could not re-read.
For the full error-handling guide, see Error Handling. For subscription lifecycle details, see Subscriptions.
Domain discovery
The protean eventstore dlq commands use the same domain discovery mechanism
as other CLI commands. See Domain Discovery for the
full resolution logic.