fix(pool): fence stale automatic-primary checkouts - #1416
Open
farnoy wants to merge 2 commits into
Open
Conversation
|
|
farnoy
force-pushed
the
fix/stale-automatic-primary-checkouts
branch
from
August 24, 2026 09:02
32c7fa0 to
33ff9dd
Compare
farnoy
force-pushed
the
fix/stale-automatic-primary-checkouts
branch
from
August 24, 2026 09:06
33ff9dd to
5bc0f70
Compare
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.
This is an AI-assisted PR. Consider this an early draft for now and feel free to recommend significant changes.
I ran into an issue where PgDog was not routing queries to my
role=autoinstances after an AWS-induced RDS Aurora failover event. The failover was caused by excessive load on CPU & drained freeable memory. After the event, I've seen a lot ofSQLSTATE 25006s - indicative of PgDog routing writer queries to what it is now a read replica. I believe PgDog should be adjusted to keep track of the latest & best evidence as to who's the primary.With the fix applied, I was able to avoid these errors in reproduction scenarios, using an automated testing harness. I only tested this patch with the settings I use:
role=auto,pooler_mode=session,query_parser=off, and having two app-side connection pools using thepgdog.role=replica/primaryoption.I will follow up with timing measurements. This patch series successfully avoids 25006 errors, but I haven't yet measured the unavailability window from the client perspective.
Summary
During a managed PostgreSQL failover, PgDog can retain an automatic target's old
Primaryrole after the backend that supplied that evidence disappears. When the database returns as a reader, a fresh session-pool checkout can then bind to that reader. The client receivesSQLSTATE 25006(cannot execute ... in a read-only transaction), and session pooling can keep returning the same bad backend until the frontend session is replaced.This change makes automatic-primary selection require valid, non-replica role evidence and adds an independent checkout-time
pg_is_in_recovery()fence before handing a backend to a write client. A backend found to be in recovery is closed, its automatic-role evidence is cleared, and selection retries another qualified target.PgDog version
Reproduced on the incident-era PgDog revision (
eff27d42) and on upstream commit5e8b8858. The proposed change is based onv0.1.54(7b40c0c2), whose additional commit changes only version metadata.Description
The failure requires a narrow sequence:
role = autowith session pooling and has identified a backend as the automatic primary.Primary, or reuses that target before fresh writer evidence is available.The resulting error is produced after a query reaches the wrong backend. A query-level retry therefore does not guarantee a fresh backend when session pooling has pinned the stale binding.
The failure is easiest to observe with sustained concurrent client traffic, short requests that identify the backend role, and a failover triggered while the clients are reconnecting. The application authentication method is not the root cause. The tested setup used IAM backend authentication and
query_parser = "off", but the relevant conditions are automatic role selection, session-pool rebinding, and a stale reader/writer topology window.How it was observed
The test used one synchronized failover event for all matrix arms. Each arm ran the same client workload and recorded, for every checkout:
pg_is_in_recovery()/ writer-versus-reader role;25006responses; andThe failover was considered relevant only when direct database role checks and the client observations bracketed the same topology change. This distinguishes a stale-reader rebind from a separate case where an already-held backend is demoted after a query has already been sent.
The compact matrix compared five arms on the same failover event:
b050a570) bundled changeAutotargets as replicas and waits for a primary on write checkout; this arm does not isolate those two behaviorsThe final slim series was then run in five synchronized events of the same matrix. The counts below are totals across those completed events; the proposed arm had 60 paired clients per event. The socket-liveness comparator (#1318) was also present in the matrix, but its partial-run total is omitted from this concise summary.
25006b050a570) bundled changeThese are strict observation-window totals and descriptive developmental evidence, not a general safety proof or a formal treatment-efficacy claim. The formal recovery deadline right-censored the completed event outcomes. The preceding ten-event broad prototype showed the same directional contrast, but those results are retained as developmental evidence only.
Root cause
Automatic role detection and checkout treated cached role state as sufficient to select a writer. Monitoring could lose, clear, or fail to refresh the LSN/recovery evidence without revoking the target's cached
Primaryrole. A later checkout therefore had no independent guard against receiving a backend that was now in recovery.The important distinction is between:
This PR addresses the second, incident-relevant path. It does not claim to migrate an in-flight PostgreSQL session or replay an ambiguous query.
Fix
The series is intentionally split into two reviewable commits:
pg_is_in_recovery()within the existing checkout timeout. A standby is force-closed, its cached automatic-role evidence is cleared, and PgDog retries another qualified primary. Probe failures fail closed rather than handing the client an unverified writer.The checkout probe is the correctness barrier. Evidence revocation prevents the load balancer from repeatedly selecting the same stale target while the topology converges.
Testing
Local validation on the final slim series:
Synchronized failover rerun: five completed events in the same five-arm matrix; the partial, non-claimable results are summarized above. Raw artifacts and provenance are retained by the submitter and are available to reproduce the summary if maintainers want the full diagnostic detail.
Configuration
No new configuration is required. The behavior applies to automatic-role targets. Static
primaryandreplicaconfigurations retain their existing semantics.Scope and limitations
This series targets fresh stale-reader checkouts after a topology change. It does not make arbitrary in-flight PostgreSQL operations replayable, and it does not transfer transaction state, cursors, temporary objects, prepared statements, or advisory locks between servers. A query whose outcome is ambiguous when a backend connection dies still requires the client/application's existing error-handling policy.
The checkout probe adds a bounded round trip for automatic-primary checkouts. That is an intentional availability/correctness tradeoff during failover: PgDog may return a checkout error while writer identity cannot be verified, rather than risk sending a write to a standby.