Problem
ProjectionActor.onReceive wraps runOnce() in a try/catch that only calls this.log.error(...), then always scheduleNextTick() at the fixed pollIntervalMs (default 1 000 ms) (src/persistence/projection/ProjectionActor.ts:53-62). The cursor only advances after a successful handle (lines 93-96 / 134-137).
One poison event therefore re-invokes the same failing handler every second, forever: an identical log line each time, no metric, no retry budget, no exponential backoff, no "skip and record", no dead-letter sink — and no way for the application to observe that a projection is wedged.
Proposed behaviour
Add a ProjectionOptions recovery strategy — fail / retry-with-backoff(n) / skip-and-report — plus an error hook carrying the offending PersistentEvent, and a stock metric/gauge for "projection stalled at offset X".
Acceptance criteria
- A handler that throws for one event: each strategy's behaviour is tested, and the log is not flooded.
Related: #433 (persistent DLQ), #199, #182
Problem
ProjectionActor.onReceivewrapsrunOnce()in a try/catch that only callsthis.log.error(...), then alwaysscheduleNextTick()at the fixedpollIntervalMs(default 1 000 ms) (src/persistence/projection/ProjectionActor.ts:53-62). The cursor only advances after a successfulhandle(lines 93-96 / 134-137).One poison event therefore re-invokes the same failing handler every second, forever: an identical log line each time, no metric, no retry budget, no exponential backoff, no "skip and record", no dead-letter sink — and no way for the application to observe that a projection is wedged.
Proposed behaviour
Add a
ProjectionOptionsrecovery strategy —fail/retry-with-backoff(n)/skip-and-report— plus an error hook carrying the offendingPersistentEvent, and a stock metric/gauge for "projection stalled at offset X".Acceptance criteria
Related: #433 (persistent DLQ), #199, #182