Commit 7b8fecc
## Summary
A cold nested subquery shape could return an initial HTTP 500 while its
dependency snapshots were still in progress. A retry succeeded once the
dependency snapshots finished.
Fixes #4715
## Problem
When an outer shape's consumer initializes,
`Consumer.all_materializers_alive?/1` subscribes to each dependency
materializer via `Materializer.subscribe/1`. That was the **only**
materializer call using the default 5s `GenServer.call` timeout —
`wait_until_ready/1` and `new_changes/3` both use `:infinity`.
A materializer can't answer any call until
`handle_continue(:start_materializer)` returns, and that blocks on
`Consumer.await_snapshot_start(..., :infinity)` until its own snapshot
starts. For a cold nested topology, the intermediate materializer itself
waits on a deeper dependency, so its snapshot can start later than 5s
after the outer consumer subscribes. The subscribe then times out,
Electric removes the outer shape, and the client's first request 500s:
```
Removing shape "..." due to abnormal shutdown: {:timeout, {GenServer, :call, [#PID, :subscribe, 5000]}}
... consumer.ex: Electric.Shapes.Consumer.all_materializers_alive?/1
... consumer.ex: Electric.Shapes.Consumer.finish_initialization/3
```
This matches the logs reported in #4715.
## Solution
`Materializer.subscribe/*` now waits with `:infinity`, consistent with
the other materializer calls. This matches the intended semantics from
the issue — *"a single cold nested shape waits for dependency
materializers and completes without an externally visible 500."*
Why this is safe:
- **Liveness** is already handled by the caller:
`all_materializers_alive?/1` does `Process.monitor(pid, ...)` *before*
subscribing, so a dead materializer surfaces as a call exit rather than
being masked by a short timeout.
- **No deadlock**: the shape-dependency graph is a DAG (outer → inner),
so no dependency can transitively wait on the outer consumer.
- **Replication is not stalled** by the longer block: the outer shape is
registered with the `ShapeLogCollector`'s `EventRouter` (via
`SetupEffects` → `ShapeLogCollector.add_shape`) only *after*
`all_materializers_alive?/1` returns, inside `initialize_event_handler`.
While the consumer is blocked in the subscribe it is not yet a routing
target, so the collector never broadcasts a transaction to it. The
overall wait is bounded by the request-level
`await_snapshot_start(outer, 45000)` budget.
## Reproduction / Test Plan
Added a `:slow` router test (`test/electric/plug/router_test.exs`) that
deterministically reproduces the bug by stalling **only** the dependency
shape's snapshot past the 5s subscribe window (via an injected
`:create_snapshot_fn`), then asserting the cold nested shape still
serves 200.
- [x] Test fails on `main` with the `{:timeout, {GenServer, :call, [_,
:subscribe, 5000]}}` signature
- [x] Test passes with the fix
- [x] `materializer_test.exs`, `consumer_test.exs`, and the `/v1/shapes
- subqueries` router tests pass
Run the reproduction:
```sh
mix test test/electric/plug/router_test.exs --include slow --only line:3505
```
---
Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4c2498a commit 7b8fecc
3 files changed
Lines changed: 79 additions & 3 deletions
File tree
- .changeset
- packages/sync-service
- lib/electric/shapes/consumer
- test/electric/plug
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
Lines changed: 9 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
106 | | - | |
107 | | - | |
108 | | - | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
109 | 115 | | |
110 | 116 | | |
111 | 117 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3478 | 3478 | | |
3479 | 3479 | | |
3480 | 3480 | | |
| 3481 | + | |
| 3482 | + | |
| 3483 | + | |
| 3484 | + | |
| 3485 | + | |
| 3486 | + | |
| 3487 | + | |
| 3488 | + | |
| 3489 | + | |
| 3490 | + | |
| 3491 | + | |
| 3492 | + | |
| 3493 | + | |
| 3494 | + | |
| 3495 | + | |
| 3496 | + | |
| 3497 | + | |
| 3498 | + | |
| 3499 | + | |
| 3500 | + | |
| 3501 | + | |
| 3502 | + | |
| 3503 | + | |
| 3504 | + | |
| 3505 | + | |
| 3506 | + | |
| 3507 | + | |
| 3508 | + | |
| 3509 | + | |
| 3510 | + | |
| 3511 | + | |
| 3512 | + | |
| 3513 | + | |
| 3514 | + | |
| 3515 | + | |
| 3516 | + | |
| 3517 | + | |
| 3518 | + | |
| 3519 | + | |
| 3520 | + | |
| 3521 | + | |
| 3522 | + | |
| 3523 | + | |
| 3524 | + | |
| 3525 | + | |
| 3526 | + | |
| 3527 | + | |
| 3528 | + | |
| 3529 | + | |
| 3530 | + | |
| 3531 | + | |
| 3532 | + | |
| 3533 | + | |
| 3534 | + | |
| 3535 | + | |
| 3536 | + | |
| 3537 | + | |
| 3538 | + | |
| 3539 | + | |
| 3540 | + | |
| 3541 | + | |
| 3542 | + | |
| 3543 | + | |
| 3544 | + | |
| 3545 | + | |
3481 | 3546 | | |
3482 | 3547 | | |
3483 | 3548 | | |
| |||
0 commit comments