fix: restart resumes the process loop and the pulse emitter - #90
Conversation
`restart()` calls `stop(true)`, which clears `running`, empties the buffer and stops the pulse emitter. The fetch loop revives itself from `restartTo`, but the process loop exits on `running` and only `start()` brought it back. A pump restarted while it was delivering kept pulling events and never delivered or checkpointed again — and stopped pulsing, so it looked dead to the control plane while its host reported healthy. This is the defect described on 2026-04-14 in Usable fragment 28875215-003d-4770-9d44-8e3488d25246. It stalled two production data-pathways pathways for 26 days (2026-08-02 → 2026-08-28): 417b8dd9-b6a3-4ce9-b254-6ea12ca97d57 and 5a32bd1c-9d0c-4aef-a813-4fee60280f8e. - `ensureProcessLoop()` starts the delivery loop whenever the pump is running with a processor and no loop is live. The fetch loop calls it every iteration, so a loop that exits for any reason comes back within one iteration instead of leaving a pump that pulls but never delivers. - `processLoopRunning` guards against a second loop when the existing one is only parked in `reserve()`. Two loops would race over the same buffer. - A loop that exits while the pump is running again — the restart landed mid-batch — restarts itself. - The restart path starts the pulse emitter again. `PulseEmitter.start()` is already idempotent. Tests: delivery continues after a restart issued from inside the handler; no second loop when one is parked; the pulse emitter is started again. All three fail on the previous behaviour. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
04956e9 to
76bf591
Compare
Follow-up independent re-review — current verdict: APPROVEReviewed head: Current verdict: APPROVE. All five earlier findings are resolved. Resolution of the five earlier findings
Exact verification evidence
Residual risks — non-blocking and pre-existing
Historical review at 76bf591 — superseded by the APPROVE verdict aboveIndependent review — PR #90Reviewed head: Verdict: REQUEST CHANGES. The push-mode fix is correct and well covered. One PR-introduced regression reproduces the same silent-stall class in the public pull-mode API, and a second wedges delivery after Command results (worktree at 76bf591)
What is correct
Finding 1 — HIGH (blocker): pull-mode consumers stop consuming permanently after
|
Problem
restart()callsstop(true), which clearsrunning, empties the buffer and stops the pulse emitter. The fetch loop revives itself fromrestartTo:But the process loop is a
while (this.running)loop and onlystart()calledstartProcessLoop(). A pump restarted while it was delivering keeps pulling events and never delivers or checkpoints again — and stops pulsing, so it looks dead to the control plane while its host still reports healthy.This is the defect written up on 2026-04-14 in Usable fragment
28875215-003d-4770-9d44-8e3488d25246.Production impact
It stalled two
data-pathwayspathways for 26 days (2026-08-02 → 2026-08-28):417b8dd9-b6a3-4ce9-b254-6ea12ca97d575a32bd1c-9d0c-4aef-a813-4fee60280f8eactiveA scoped replay asked for one flow type; the consumer restarted every source, and every restarted pump went quiet.
Change
ensureProcessLoop()starts the delivery loop whenever the pump is running with a processor and no loop is live. The fetch loop calls it once per iteration, so a loop that exits for any reason comes back within one iteration.processLoopRunningguards against a second loop when the existing one is only parked inreserve()— two loops would race over the same buffer.pulseEmitter.start()again. It is already idempotent.Tests
Three new cases in
data-pump-restart.test.ts, all of which fail on the previous behaviour:restart()issued from inside the handlerSuite: 57 pass / 0 fail.
tsc --noEmitandprettier --checkclean.🤖 Generated with Claude Code