Repository navigation
Bound subscriber statements so loops reconnect after a silent primary loss - #262
Merged
Merged
Conversation
… loss A fetch or terminal write on a connection whose primary vanished without closing it (power loss, network partition) hung until the kernel's TCP timeout, so the subscriber stopped consuming after a failover. Each fetch, delete, batch delete and retry update is now capped at the new query_timeout_seconds subscriber option (default: lease_ttl_seconds); on timeout the connection is invalidated and the loop reconnects. Adds failover drills (just test-dr): a primary/streaming-replica compose cluster where the primary is either killed or cut off from the network and the replica is promoted. Not part of CI.
Benchmark gate✅ gate passed
Gated (fails the build): |
The floors job runs pytest . and sets POSTGRES_DSN, so the drills were collected and tried to connect to a cluster that does not exist.
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.
Problem
When the Postgres primary disappears without closing its connections (host power loss, network partition), the subscriber stops consuming for good. The fetch query and the terminal writes have no client-side timeout, so they wait on the dead socket until the kernel gives up on TCP retransmits (about 15 minutes on Linux by default). The LISTEN health probe can't help, because it runs between fetches and the fetch itself is the call that's stuck.
A primary that dies cleanly (process killed) was already fine: the next statement fails, and the existing reconnect-with-backoff takes over.
Fix
asyncio.timeout(query_timeout_seconds). On timeout the connection is invalidated, so it gets closed without another round trip on the dead socket, and the error reaches the reconnect loop.query_timeout_seconds, available on@broker.subscriber,OutboxRouteand the FastAPI router. It defaults toNone, which meanslease_ttl_seconds. Values must be> 0, finite, and at mostlease_ttl_seconds. A fetch or write still running past the lease has already lost the row, so a larger value buys nothing.2 × query_timeout_seconds. The fetch loop hits the cap first. The worker then hits it again on its own stale connection once it gets a row. Bringing that down to one timeout would mean recycling the worker's connection whenever the fetch loop reconnects. I left that out of this PR.Failover drills
just test-drbrings up a primary and a streaming replica (docker-compose.dr.yml, replica built withpg_basebackup -R). The app connects through a multi-host URL withtarget_session_attrs=primary. Each scenario runs on a fresh cluster:kill:docker killthe primary, thenpg_promote()the replica.blackhole: disconnect the primary from the Docker network, which is a silent drop with no RST, then promote the replica.The test checks three things: every message published after the failover is handled within 90s, messages handled before it stay handled, and a NOTIFY wakeup works afterwards. The drills sit in
tests_dr/, outsidetestpaths, and are excluded from coverage, so CI doesn't run them.Local results (OrbStack):
query_timeout_seconds=10One limitation: after a network disconnect, Docker's DNS stops resolving the old primary's name, so new connection attempts to it fail quickly. A real partition would make them wait for asyncpg's connect timeout (60s by default) before trying the next host.
Tests
test_hung_fetch_times_out_and_fetch_loop_reconnects;test_hung_terminal_write_times_out_and_worker_reconnects.test_query_timeout_bounds_hung_fetch_below_lease_ttluses a 600s lease with a 0.5s query timeout, which shows the option is what actually bounds the hang.OutboxRouteand the FastAPI router.Docs
docs/usage/subscriber.mdcovers the engine URL, recovery time per failure mode, and what asynchronous replication can lose.AGENTS.md(for the new recipe) are updated.