Skip to content

fix(sync-service): drop subquery shapes on restart instead of restoring them#4735

Open
robacourt wants to merge 4 commits into
mainfrom
rob/drop-subqueries-on-restart
Open

fix(sync-service): drop subquery shapes on restart instead of restoring them#4735
robacourt wants to merge 4 commits into
mainfrom
rob/drop-subqueries-on-restart

Conversation

@robacourt

Copy link
Copy Markdown
Contributor

Summary

Restoring shapes that involve subqueries consistently across a server restart has proven too fragile: keeping an outer shape's on-disk view in sync with its dependency materializer across a restart could diverge, producing incorrect results (or a 409 must-refetch) after the server comes back up.

Rather than keep chasing those restore-path consistency bugs, this drops every shape involved in a subquery on startup and lets clients re-request them from scratch.

Changes

  • Replace eagerly_start_subquery_shape_consumers/1 with drop_subquery_shapes/1 in shape_cache.ex, called from handle_continue(:wait_for_restore, …) before ShapeLogCollector.mark_as_ready/1, so no change ever reaches a dropped shape.
  • On boot, remove the transitive closure [handle | shape_dependencies_handles] for every shape with a non-empty shape_dependencies, via a single ShapeCleaner.remove_shapes/2 call. This covers nested subqueries automatically: an intermediate dependency that itself has a subquery matches the same filter and contributes its own dependencies.
  • The lazy restore_shape_and_dependencies/3 path (used by start_consumer_for_handle and fresh creation) is untouched; after startup there are no subquery shapes left to restore.
  • Update shape_cache_test.exs to assert the drop behaviour (subquery shape + its materializers are gone after restart) instead of restore.

Test Plan

  • mix test test/electric/shape_cache_test.exs — passes (drop-on-restart + guard tests).
  • oracle_restore_test.exs bug 1 (subquery-shape-diverges-after-restart regression) now passes with the full stack.

Notes

Supersedes the restore path added in #4666 / #4715 for the subquery-restart case. The remaining oracle_property_test failures under load are pre-existing and unrelated to this change (a checker-timeout/liveness artifact plus a separate 409 flake); they reproduce with RESTART_SERVER_EVERY=0.


Generated with Claude Code

robacourt and others added 2 commits July 20, 2026 15:44
…ng them

Restoring shapes that involve subqueries consistently across a restart has
proven too fragile: the outer shape's on-disk view and its dependency
materializer can diverge, producing incorrect results (or a 409 must-refetch)
after the server comes back up.

Rather than keep chasing those consistency bugs, drop every shape involved in
a subquery on startup and let clients re-request them from scratch. On boot,
`drop_subquery_shapes/1` (replacing `eagerly_start_subquery_shape_consumers/1`)
removes the transitive closure `[handle | shape_dependencies_handles]` for each
shape with a non-empty `shape_dependencies`, before ShapeLogCollector starts
dispatching events so no change ever reaches a dropped shape.

The targeted regression `oracle_restore_test.exs` bug 1 now passes; the restart
property test survives many more restart cycles before hitting a separate,
pre-existing cold subquery snapshot bug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploy Preview for electric-next ready!

Name Link
🔨 Latest commit 07ffc00
🔍 Latest deploy log https://app.netlify.com/projects/electric-next/deploys/6a5f9043494d5e0008bd119c
😎 Deploy Preview https://deploy-preview-4735--electric-next.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.05%. Comparing base (7b8fecc) to head (566f3da).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4735   +/-   ##
=======================================
  Coverage   60.04%   60.05%           
=======================================
  Files         397      397           
  Lines       43766    43766           
  Branches    12588    12590    +2     
=======================================
+ Hits        26278    26282    +4     
+ Misses      17407    17403    -4     
  Partials       81       81           
Flag Coverage Δ
packages/agents 72.64% <ø> (ø)
packages/agents-mcp 77.70% <ø> (ø)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.73% <ø> (-0.05%) ⬇️
packages/agents-server 75.65% <ø> (+0.17%) ⬆️
packages/agents-server-ui 8.32% <ø> (ø)
packages/electric-ax 51.06% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.89% <ø> (ø)
packages/y-electric 56.05% <ø> (ø)
typescript 60.05% <ø> (+<0.01%) ⬆️
unit-tests 60.05% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

robacourt and others added 2 commits July 21, 2026 16:58
The sync service now removes every shape involved in a subquery on startup to prevent consistency issues.
…tore

The ShapeLogCollector restores routing indexes independently from ShapeStatus
in handle_continue(:restore_shapes), and it starts before ShapeCache. The
startup drop removed subquery shapes from ShapeStatus/storage but relied on
ShapeCleaner -> ShapeLogCollector.remove_shape to clear the collector routing;
that call goes through the RequestBatcher, which deliberately returns before the
removal is applied. So a restart could reinstate dropped subquery handles into
the collector's routing indexes and mark_as_ready could run before the async
cleanup landed.

Skip the whole subquery hierarchy in the collector's restore too, using a shared
ShapeStatus.subquery_shape_handles/1 helper (also used by the ShapeCache drop),
so a dropped handle is never added to routing in the first place. Add a
deterministic test that restarts only the collector and asserts the subquery
hierarchy is absent from ShapeLogCollector.active_shapes/1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant