Don't crash ShapeLogCollector on introspection failures in the replication path#4565
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4565 +/- ##
==========================================
+ Coverage 57.16% 58.78% +1.61%
==========================================
Files 340 383 +43
Lines 39535 42260 +2725
Branches 11491 12104 +613
==========================================
+ Hits 22599 24841 +2242
- Misses 16898 17343 +445
- Partials 38 76 +38
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:
|
Claude Code ReviewSummaryIncremental review (iteration 2). Since iteration 1 the branch was rebased onto current What's Working Well (this iteration)
Issues FoundCritical (Must Fix)None. Important (Should Fix)None. The iteration-1 "Important" item (permanent introspection errors → unbounded paused-replication loop on the hot path) is unchanged and remains a deliberate, documented trade-off — alive-and-paused beats crash-looping the Suggestions (Nice to Have)
Issue ConformanceUnchanged from iteration 1: no formal GitHub issue is linked, but the PR body and changeset reference #4563 with a per-signature breakdown that the implementation maps onto cleanly. Adding a Previous Review StatusAll three iteration-1 suggestions addressed:
The iteration-1 "Important" finding was acknowledged as an intentional trade-off and is unchanged; no regressions observed. Review iteration: 2 | 2026-06-15 |
…eplication path
The replication hot path (Partitions, pk-column lookup, shape restore)
only handled {:error, :connection_not_available} from the inspector.
Every other legal inspector return — :table_not_found for dropped
tables, in-band DB errors like out-of-memory or statement_timeout, and
connection-class errors returned in-band by Postgrex — crashed the
ShapeLogCollector, took down the one_for_all shapes supervision tree
and put the replication client into its 10-minute noproc retry loop.
- Partitions.handle_relation/2: ignore :table_not_found, propagate
other errors instead of raising a CaseClauseError
- Partitions.add_shape/3: return {:error, reason} instead of raising
- pk_cols_of_relation: key changes on the full record when the table
was dropped before introspection (same fallback as PK-less tables)
- ShapeLogCollector: log and reply with the retryable
:connection_not_available error for unexpected introspection
failures instead of crashing
- shape restore: retry introspection in place while the connection
pool is unavailable instead of crashing with a MatchError
- DirectInspector: classify in-band connection-class query errors
(e.g. "ssl recv: closed") as :connection_not_available
Fixes part of #4563.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All branches returned the same {:error, ...} tuple shape, so return the
bare error value from normalize_query_error/1 and wrap it at the two call
sites instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both handle_txn_fragment/2 and handle_relation/2 had a dedicated
:connection_not_available clause plus a near-identical {:error, reason}
clause that returned the same {{:error, :connection_not_available}, state}
tuple with the same explanatory comment. Collapse each into a single
{:error, reason} clause and factor the return value into a shared
normalize_inspector_error/1 helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "retries introspection until the connection becomes available" test only checked that the collector process stayed alive, which would pass even if the retry logic silently skipped the shape. Assert that the connection error was actually hit (retry exercised) and that the restored shape routes a transaction to its consumer, proving recovery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Explain why restore_partitions_for_shape retries introspection in place rather than letting the error fall through: there is no upstream handler that recovers gracefully (restore runs in handle_continue, the SLC's own supervisor is max_restarts: 0, and the one_for_all parent would restart the whole shape subsystem on every transient pool-warmup failure). Bound the total blocking wait to 4s (40 * 100ms), comfortably under the supervisor's default 5s shutdown timeout, so an in-flight restore retry doesn't risk a brutal kill on shutdown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a6f1737 to
a4dcc7d
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4dcc7d617
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
This PR has been released! 🚀 The following packages include changes from this PR:
Thanks for contributing to Electric! |
Fixes the crash family from #4563: the replication hot path (
Partitions, pk-column lookup, shape restore) only handled{:error, :connection_not_available}from the inspector, so every other legal inspector return crashed the ShapeLogCollector, took down the:one_for_allshapes supervision tree, and put the replication client into its 10-minute noproc retry loop.Changes
Partitions.handle_relation/2: handles:table_not_found(table dropped/renamed between the WAL record and introspection — nothing left to track, mirroring the existingadd_shapebehavior) and propagates other errors instead of crashing with aCaseClauseError.Partitions.add_shape/3: returns{:error, reason}instead of raising aRuntimeErroron unexpected introspection errors; the shape-registration path propagates the error to the caller.pk_cols_of_relation: when the table was dropped before introspection, changes are keyed on the full record (the same fallback used for tables without a primary key) instead of crashing inUtils.map_while_ok; handling of the dropped table is left to the affected shapes further down the stack.{:error, :connection_not_available}— the one error the replication client knows how to recover from — pausing replication until introspection succeeds instead of crashing the collector.MatchErrorin a restart loop. Skipping the shape was not an option: nothing re-registers restored shapes later, so a skipped shape would silently stop receiving updates. If the error persists past ~10s the restore still fails, but with a descriptive error.DirectInspector: connection-class errors returned in-band by Postgrex (e.g."ssl recv: closed") are classified as:connection_not_availablevia the existingElectric.DbConnectionErrorclassifier, so they take the established recovery path instead of leaking through as strings.Production context
In Electric Cloud these crashes account for most of the "retry budget" Sentry noise:
{:case_clause, :table_not_found},{:case_clause, {:error, "ERROR 53200 (out_of_memory) ..."}},query_canceled,"ssl recv: closed", the restoreMatchError, and — downstream of all of them — thousands of(EXIT) no processdispatch errors per incident while the supervision tree restarts. See #4563 for the breakdown.Notes for review
partitions_test,shape_log_collector_testand a newdirect_inspector_test).🤖 Generated with Claude Code