Repository navigation
Commit ca04983
fix(sync-service): prevent permanent flush-boundary stalls from unexpected consumer lifecycle transitions (#4730)
## Problem
The FlushTracker pins `confirmed_flush_lsn` for the whole stack on the
minimum incomplete flush entry, and cleanup of entries is purely
event-driven: either new traffic routed to the same shape (publish-time
DOWN detection) or an explicit `ShapeLogCollector.remove_shape/2`
arriving via the ShapeCleaner chain. Several real-world paths break both
— a storage error that crashes the consumer *and* aborts its
`terminate/2` before `handle_writer_termination` runs, a removal chain
that dies between the `ShapeStatus` removal and the SLC removal
(unretryable: the retry short-circuits at `{:error, :data_removed}`),
untagged `{:shutdown, x}` exit reasons classified as benign, and a
consumer that stays alive but stops flushing. On a subsequently-quiet
table any of these pins the WAL ack forever, causing unbounded WAL
growth on Postgres. PR #4713 has a minimal reproduction of the stall.
## Solution
The SLC becomes the authority on consumer-death handling for flush
accounting. Invariant: an incomplete FlushTracker entry always has a
live monitor watching the pid responsible for completing it, and a
timestamp bounding how long it may sit without flush progress.
- **Writer monitors**: the SLC monitors every consumer with an
incomplete FlushTracker entry (created when a commit-fragment publish
tracks the shape, released when the entry completes or the shape is
removed). DOWNs are classified by exit reason: crashes unpin the flush
boundary immediately and trigger shape invalidation; deliberate-cleanup
DOWNs (`{:shutdown, :cleanup}`) unpin only; bare `:shutdown`/`:killed`
are assumed to be supervisor teardown and left alone.
- **Stall detection**: incomplete entries carry a `last_progress_at`
timestamp refreshed only by flush notifications. A periodic tick finds
shapes whose entries make no flush progress for
`ELECTRIC_FLUSH_STALL_GRACE_PERIOD` (default 60s) and invalidates them
via the normal ShapeCleaner path, re-arming on each fire so a lost
cleanup chain retries instead of pinning forever. Misclassified DOWNs
self-heal through the same mechanism.
- **Stall challenge-response**: a stalled entry whose writer is still
alive is challenged (`:verify_flush_progress`) before being acted on. A
consumer answers only while legitimately deferring flush notifications
(buffering ahead of PG snapshot info, subquery move-in awaiting splice);
the answer re-arms the entry's grace period so healthy deferral phases
are not misread as stalls. Only a suspect still stalled and silent at
the next check — or a shape whose writer is already dead and
demonitored, which skips the challenge — is invalidated. Consumers stay
silent in steady state: no periodic messaging, and a wedged consumer
can't answer, so the backstop stays armed.
- **Reason normalization**: the benign clause of
`handle_writer_termination` shrinks to exactly
`:normal`/`:killed`/`:shutdown`, so untagged `{:shutdown, x}` reasons
now trigger removal; tagged dependency-materializer shutdowns map to
`stop_and_clean`; suspend additionally requires all txn state flushed
and notified (guaranteeing a suspended consumer never leaves an
incomplete entry behind); `Consumer.stop` escalates a wedged consumer to
a kill after the stop call times out.
- ShapeCleaner re-issues the idempotent SLC removal even when the shape
is already gone from `ShapeStatus`, healing pins left by removal chains
that died partway through.
Telemetry: new `[:electric, :flush_tracker, :writer_down]` and
`[:electric, :flush_tracker, :stall_detected]` events.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Garry Hill <garry@electric-sql.com>1 parent 88793e7 commit ca04983
18 files changed
Lines changed: 1244 additions & 140 deletions
File tree
- .changeset
- packages
- electric-telemetry/lib/electric/telemetry
- sync-service
- config
- lib/electric
- replication
- shape_log_collector
- shape_cache
- shapes
- test
- electric
- replication
- shape_log_collector
- shapes
- support
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
138 | | - | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
139 | 141 | | |
140 | 142 | | |
141 | 143 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
173 | 173 | | |
174 | 174 | | |
175 | 175 | | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
176 | 179 | | |
177 | 180 | | |
178 | 181 | | |
| |||
276 | 279 | | |
277 | 280 | | |
278 | 281 | | |
| 282 | + | |
279 | 283 | | |
280 | 284 | | |
281 | 285 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
154 | 154 | | |
155 | 155 | | |
156 | 156 | | |
| 157 | + | |
157 | 158 | | |
158 | 159 | | |
159 | 160 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
104 | 104 | | |
105 | 105 | | |
106 | 106 | | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
107 | 111 | | |
108 | 112 | | |
109 | 113 | | |
| |||
0 commit comments