Skip to content

fix(sync-service): start missing dependency consumers before their parent - #4779

Merged
alco merged 4 commits into
mainfrom
fix/start-dependency-consumer-on-demand
Sep 1, 2026
Merged

alco merged 4 commits into
mainfrom
fix/start-dependency-consumer-on-demand

Conversation

@alco

@alco alco commented Aug 27, 2026 •

Copy link
Copy Markdown
Member

This PR is based on the code from #4774 and supersedes it.

Problem

A subquery dependency can remain registered in ShapeStatus with a completed snapshot but no running consumer. When a new parent resolves its subquery to that existing handle, parent startup assumes the dependency consumer already exists. Its materializer fails to subscribe, the parent is invalidated, and every retry repeats the same failure because the dependency remains registered.

One way to reach this state is for a dependency to become orphaned before restart. The restart pruning pass only recognizes dependencies through surviving parents, while restored standalone shapes start consumers lazily when transaction routing first needs them.

Solution

When ShapeCache resolves an existing shape while creating an inner shape, ensure its consumer is running before starting the parent. Missing consumers are restored through the same dependency-aware path used by transaction routing, while preserving is_subquery_shape?: true so the recovered dependency buffers complete transactions.

The shared restoration helper also removes duplicated consumer-starting logic.

Test

The regression test constructs the registered/completed-snapshot/no-consumer state, requests a new parent, and verifies that:

  • the existing dependency handle is reused;
  • its consumer is restarted before the parent initializes;
  • the recovered dependency uses write_unit: :txn.

Focused verification: mix test test/electric/shape_cache_test.exs:1081.

Follow-up

A pre-existing edge case remains when an already-running standalone consumer is later reused as a dependency: its write mode must be promoted at a transaction boundary. That behavior is addressed in #4780.

@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.25%. Comparing base (f0e1904) to head (5ada318).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4779      +/-   ##
==========================================
- Coverage   60.06%   59.25%   -0.82%     
==========================================
  Files         397      379      -18     
  Lines       43772    42653    -1119     
  Branches    12589    12333     -256     
==========================================
- Hits        26290    25272    -1018     
+ Misses      17401    17339      -62     
+ Partials       81       42      -39     
Flag Coverage Δ
packages/agents 72.77% <ø> (ø)
packages/agents-mcp 77.70% <ø> (ø)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.73% <ø> (ø)
packages/agents-server 75.65% <ø> (ø)
packages/agents-server-ui 8.32% <ø> (ø)
packages/electric-ax 51.06% <ø> (ø)
packages/experimental ?
packages/react-hooks ?
packages/start 82.83% <ø> (ø)
packages/typescript-client ?
packages/y-electric 56.05% <ø> (ø)
typescript 59.25% <ø> (-0.82%) ⬇️
unit-tests 59.25% <ø> (-0.82%) ⬇️

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.

Jacob Clark and others added 4 commits September 1, 2026 11:15
… when its materializer starts

A dependency (subquery) shape can be registered in ShapeStatus with a
completed snapshot but no running consumer: consumers for restored
shapes start lazily on their first transaction, and a dependency shape
orphaned by its parent's removal is restored as a plain shape on
restart (prune_subquery_shapes/1 only matches dependencies through a
surviving parent).

A new parent shape then resolves its subquery to the orphaned handle,
its dependency materializer crashes calling the missing consumer, and
the parent is invalidated while the dependency stays registered, so
every retry fails the same way.

Have the materializer start the missing consumer via
ShapeCache.start_consumer_for_handle/3, the same mechanism the
transaction routing path uses, instead of assuming one is running.
…terializer

`start_shape/3` assumes that the consumers of a shape's dependencies are
already running, but `maybe_create_shape/2` returned an existing inner
shape handle without checking that. Enforce the precondition there: when
an inner shape resolves to an existing handle with no registered
consumer, restore it via `restore_shape_and_dependencies/3` before the
parent is started.

Compared to starting the consumer from the materializer's init:

- runs inside ShapeCache, so no materializer -> ShapeCache round trip
- passes the inner shape `opts` through, so the restored consumer keeps
  `is_subquery_shape?: true` (write_unit=txn) like every other
  dependency consumer; the test now asserts this
- a failure to start the dependency surfaces as a create error for the
  request instead of a materializer crash that invalidates the parent
- the materializer no longer needs to change (the previous version also
  introduced a clause-grouping compiler warning that fails CI's
  --warnings-as-errors check)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yof2snscrAFy7ToQNZgq4
@alco
alco force-pushed the fix/start-dependency-consumer-on-demand branch from e75eb2a to 5ada318 Compare September 1, 2026 09:15
@netlify

netlify Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ Deploy Preview for electric-next ready!

Name Link
🔨 Latest commit 5ada318
🔍 Latest deploy log https://app.netlify.com/projects/electric-next/deploys/6a9697a5bf3a660008764599
😎 Deploy Preview https://deploy-preview-4779--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.

@alco
alco merged commit ab53baa into main Sep 1, 2026
50 of 54 checks passed
@alco
alco deleted the fix/start-dependency-consumer-on-demand branch September 1, 2026 09:24
alco added a commit that referenced this pull request Sep 1, 2026
…n writes (#4780)

## Background

Shape consumers without their own subquery dependencies start with
`write_unit: :txn_fragment`. Consumers known to be inner shapes start
with `write_unit: :txn`, because dependency materializers and
outer-shape evaluation require transaction-atomic updates.

The startup hint is not sufficient when an already-running consumer is
later adopted as a dependency. This can happen in two pre-existing
sequences:

- a standalone shape is requested directly and later reused as a
parent's subquery dependency;
- after restart, transaction routing lazily recovers an orphaned
dependency as a standalone shape before a new parent reuses it.

In both cases the dependency materializer can subscribe to a consumer
that remains in fragment-write mode.

## Fix

Treat materializer subscription as the authoritative signal that a
consumer is an inner shape and promote it to whole-transaction writes.

If no transaction is in progress, promotion is immediate. If the
subscription arrives during a fragmented transaction, that transaction
finishes in fragment mode and promotion occurs at its commit boundary.
This avoids mixing already-written fragments with a buffered tail of the
same transaction.

The existing `is_subquery_shape?` startup option remains a fast path for
consumers known to be dependencies when they start.

## Tests

- verifies an already-running standalone consumer is reused by a parent
and promoted from `:txn_fragment` to `:txn`;
- verifies subscription during an in-flight fragmented transaction
defers promotion until commit;
- covers the immediate and deferred state transitions directly;
- full relevant files: `110 passed, 1 excluded` across ShapeCache,
consumer state, and consumer tests.

## Stack

This PR is stacked on #4779 and should be reviewed against
`fix/start-dependency-consumer-on-demand` until that PR merges.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

This PR has been released! 🚀

The following packages include changes from this PR:

  • @core/sync-service@1.8.0

Thanks for contributing to Electric!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant