fix(sync-service): start the consumer of a dependency shape on demand when its materializer starts - #4774
Open
Jacob-Clark-Tipalti wants to merge 1 commit into
Conversation
… 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.
✅ Deploy Preview for electric-next ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
In production (sync-service 1.7.11) we observed requests for a shape with a subquery failing with a 500 on every attempt. The flow for each request:
The request creates the parent shape, which resolves its subquery to an already-registered dependency shape handle.
A materializer is started for the dependency, but its startup crashes because the dependency shape has no running consumer:
The parent consumer's initialization then finds no live materializer to subscribe to and invalidates itself:
The request process, blocked in
await_snapshot_starton the parent consumer, crashes when the consumer stops, producing the 500:This path is problematic because nothing in it restarts the dependency's consumer or removes the dependency's registration: only the parent is cleaned up. The client's retry creates a new parent, resolves the subquery to the same dependency handle, and fails identically. We observed a single user looping through this for 17 minutes (28 requests, 28 500s) with no recovery, and the same loop affecting tens of users per day.
Why is the dependency's consumer not running?
We do not know. What we can say is that the state exists in production: the dependency handle is registered in ShapeStatus with a completed snapshot, but no consumer process is running and the ConsumerRegistry lookup returns nil (visible in the
GenServer.call(nil, ...)above).One guess: the dependency was orphaned before a restart (parents are removed by several cleanup paths that leave their dependencies registered), and
prune_subquery_shapes/1only matches dependencies through a surviving parent, so an orphan is restored as a plain shape, whose consumer only starts lazily on transaction routing. The timeline fits (the failing handles predate the pod's last restart), but we have not confirmed this is the producer, and there may be others.The (partial) solution
Have the materializer start the missing consumer via
ShapeCache.start_consumer_for_handle/3, the same mechanism the transaction routing path uses to lazily start consumers, instead of assuming one is running. The dependency's snapshot and log are intact on disk, so this recovers the existing shape rather than discarding it, and the parent request succeeds.Adds a regression test that constructs the registered-but-no-consumer state and asserts a new parent request recovers the existing dependency (same handle, consumer restarted) instead of looping.
This does not address whatever root cause leaves a registered dependency shape without a running consumer in the first place; it makes the state recoverable when it is encountered.