Restore subquery shapes correctly after a server restart (materializer move replay)#4681
Restore subquery shapes correctly after a server restart (materializer move replay)#4681robacourt wants to merge 5 commits into
Conversation
…l timeout The per-dependency moves-position is now staged when the move pipeline drains and only committed+persisted once the writer confirms the flush has passed the move's splice (and, at terminate, after the writer has been flushed). This keeps the persisted position from running ahead of durable storage across a restart, which could otherwise leave a subquery shape permanently missing rows. Also raise the oracle-restore test's long_poll_timeout: a very short one trips a separate post-restart long-poll readiness race (bug 3), unrelated to the subquery-restore behaviour under test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4681 +/- ##
==========================================
- Coverage 60.15% 60.03% -0.13%
==========================================
Files 410 395 -15
Lines 44377 43747 -630
Branches 12580 12580
==========================================
- Hits 26697 26264 -433
+ Misses 17602 17405 -197
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:
|
It is called from terminate/2 after terminate_writer/1 has popped :writer off
the state, so the value is no longer typed as %State{} and the %State{} clauses
were flagged as never matching. Match a bare map and read the fields via
Map.get instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude Code ReviewSummaryThis PR fixes optimized streaming subquery shapes diverging from Postgres after a graceful restart (bug 5): it persists a per-dependency "moves-applied-up-to" source-LSN position, replays missed materializer moves on subscribe, and couples position persistence to durable writer flushes. The design is careful and well-tested — the staged→committed durability coupling is the right shape for the "never persist a position ahead of storage" invariant. Overall a solid, high-quality change; the notes below are mostly edge-cases and polish. What's Working Well
Issues FoundCritical (Must Fix)None found. Important (Should Fix)1. Replay drops move-in/move-out control messages for nested subquery dependencies File:
The existing This only matters when a materializer's source (dependency) shape log itself contains move events — i.e. a nested subquery (a subquery shape that is a dependency of another subquery shape). Single-level shapes (the case this PR targets and tests) are unaffected, since the inner shape is a plain shape whose log has only regular changes. But the fact that Please confirm whether nested optimized-subquery dependencies are reachable. If they are, this replay path silently drops their missed moves on restart — reintroducing the exact divergence class this PR fixes, for the nested case. If nesting is not supported, a guard/assert (or a comment noting the assumption) would make the limitation explicit. Suggestions (Nice to Have)2. Decoder duplication. 3. Broad rescue in 4. Duplicated comment block on 5. Write amplification (minor). Issue Conformance
Review iteration: 1 | 2026-07-02 |
Summary
Fixes optimized streaming subquery shapes diverging from Postgres after a graceful server restart — bug 5 from the restart-aware oracle property-test triage (#4648 /
bugs.md). A shape likeissues WHERE project_id IN (SELECT id FROM projects WHERE active = true)could permanently lose (or retain stale) rows after a restart.Problem
A dependency move (move-in / move-out) driven by the materializer is asynchronous: a move-in fires a PG query and only writes its rows to the shape log when that query completes and splices. If a graceful restart fires while a move is still in flight — or queued behind one, or sitting unprocessed in the mailbox, or not yet even emitted by the materializer — the move is lost. On restart the materializer restores its view (which reflects the move) but the outer shape's storage does not, and nothing re-drives the move, so the shape diverges from Postgres.
Solution (per commit)
Add failing test— a deterministictest_against_oraclerestart reproduction (:oracle_restore_optimized_refetch).Prevent shape removal and request crashes during restart— during a restart a source consumer's inline call to its dying materializer could exit with:noproc, crash the consumer, and cascade into removing the shape from disk (→ 409). The consumer now absorbs that exit and lets the monitored:DOWNdrive a clean stop.Replay missed moves after restart— the materializer tags each emitted move with its source LSN and, on subscribe, replays the moves a consumer is missing from its persisted per-dependency position; the consumer catches up. Because the consumer simply re-applies whatever the materializer replays, every restart-timing class (in-flight, queued, mailbox, not-yet-emitted) is covered by construction.Couple move-position persistence to durable flush— the per-dependency moves-position is staged when the move pipeline drains and only committed/persisted once the writer confirms the flush has passed the move's splice (and at terminate, after the writer is flushed), so the persisted position can never run ahead of durable storage across a restart. Also raises the test'slong_poll_timeoutoff an artificially short value that trips a separate post-restart long-poll readiness race (bug 3).Test Plan
oracle_restore_test.exs:oracle_restore_optimized_refetch— view-mismatch eliminated; deterministic across 100+ runs on healthy Postgres.--only oracle_restore_bug_1still passes.test/electric/shapes/+test/electric/replication/— 0 failures.🤖 Generated with Claude Code