Skip already-processed transactions on restart#4668
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4668 +/- ##
==========================================
- Coverage 60.15% 59.99% -0.17%
==========================================
Files 410 395 -15
Lines 44348 43747 -601
Branches 12582 12580 -2
==========================================
- Hits 26677 26244 -433
+ Misses 17593 17425 -168
Partials 78 78
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Replace the complete-transaction offset check and process_txn_fragment's fragment_already_processed?/2 with one offset-only handle_txn_fragment clause placed after the buffering? clause, routing to the existing skip_txn_fragment. The dedup now lives in one place and covers single- and multi-fragment paths uniformly. Behaviour-preserving. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the multi-fragment replay path (BEGIN/middle/COMMIT fragments skipped at the offset-dedup clause without setting up pending_txn), which the existing single-fragment replay test doesn't exercise. Reword the changeset to drop implementation details that no longer match after the dedup consolidation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude Code ReviewSummaryThis PR fixes a real, well-diagnosed restart bug: the persistent replication slot replays already-applied transactions, and the complete-transaction fast paths short-circuited before the offset-dedup check — duplicating ops in the shape log and crashing dependent subquery materializers. Hoisting the dedup into a single What's Working Well
Issues FoundCritical (Must Fix)None. Important (Should Fix)None. Suggestions (Nice to Have)
Issue ConformanceNo linked issue on the PR (minor process note — PRs should reference the issue they address), but the body ties this to bug 2 from the restart-aware oracle property-test triage (#4648 / Previous Review StatusAll iteration 1 and 2 feedback remains addressed. The one open discussion point from iteration 2 — the straddle invariant being a documented-but-unenforced impossibility — has now been hardened into an explicit loud failure (commit Review iteration: 3 | 2026-07-01 |
latest_offset advances per written fragment, not only at commit, so reword the justification: the whole-transaction skip is safe on the replay path because the restored latest_offset is commit-aligned (Storage.fetch_latest_offset/1) and the slot replays whole transactions from a commit boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Skipping a BEGIN fragment via the already-applied clause now leaves pending_txn: nil. In normal operation process_txn_fragment is only reached with pending_txn set (the BEGIN fragment creates it), so this is safe. But a transaction straddling latest_offset would reach it with pending_txn: nil and crash on `nil.consider_flushed?`. Add an explicit pending_txn: nil clause that raises a clear, documented error instead of the implicit BadMapError. The straddle is ruled out by the commit-aligned restore + whole-transaction replay invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
On restart, the persistent replication slot can replay transactions the consumer has already applied and persisted. The consumer deduplicated these on its multi-fragment path, but the complete-transaction fast paths short-circuited before reaching that check — so a replayed transaction was re-written to the shape log (duplicating ops) and re-notified to dependent subquery materializers, which re-applied it and crashed with
Key ... already exists.This is bug 2 from the restart-aware oracle property-test triage (see #4648 /
packages/sync-service/bugs.md).Problem
After a
StackSupervisorrestart, the source consumer reconnects to the persistent slot and replays from its confirmed LSN, which can lag the shape's on-disk offset. Complete single-fragment transactions (the common case — one statement per txn) took the fast path and were re-processed:"Key ... already exists"). That crash then cascades into tearing down and removing the healthy dependent shapes and returning409 must-refetchto clients.Solution
Consolidate the offset dedup into a single
handle_txn_fragment/2clause that skips any fragment already at or belowlatest_offset(routing to the existingskip_txn_fragment), placed before the complete-transaction fast paths. The dedup now lives in one place and covers single- and multi-fragment paths uniformly, replacing the separatefragment_already_processed?/2check. Normal operation is untouched: new transactions haveoffset > latest_offset, so the guard is false and they fall through to the existing clauses.This relies on a transaction's fragments being entirely at-or-below or entirely above
latest_offset(which only advances at commit boundaries) — the same assumption the previous per-fragment check already made.Test Plan
test/electric/shapes/consumer_test.exs— two regression tests (single-fragment and multi-fragment) replay an already-applied transaction straight to the consumer, bypassing the log collector's own dedup (as happens on restart with a fresh collector), and assert it is skipped: noappend_to_log!, no:new_changesnotification, log unchanged. Fail without the fix.test/electric/shapes/+test/electric/replication/— 937 tests, 100 doctests, 6 properties, 0 failures.Generated with Claude Code