pg_autoctl: retry perform_failover on transient monitor deadlocks (#1004) - #1171
Merged
Conversation
) perform_failover() on the monitor takes the same LockFormation()/ LockNodeGroup() advisory locks every other node-mutating entry point does, so it can hit a genuine Postgres deadlock racing a concurrent node_active()/health-check write -- exactly the SQLSTATE 40P01 signature reported in #1004: ERROR Monitor ERROR: deadlock detected ERROR Monitor Process ... waits for ExclusiveLock on advisory lock ERROR Monitor CONTEXT: while updating tuple ... in relation "node" ERROR SQL query: SELECT pgautofailover.perform_failover($1, $2) FATAL Failed to perform failover/switchover, see above for details pg_autoctl has had a retry policy for exactly this class of transient error since 2020 (monitor_retryable_error(), covering deadlock_detected, serialization_failure, and a few others) -- monitor_start_maintenance, monitor_stop_maintenance, and monitor_register_node all use it. But monitor_perform_failover() and monitor_perform_failover_allow_data_loss() were never wired into it: both just ran their query once and failed the whole command on any error, deadlock or not. That gap is why #1004's `pg_autoctl perform switchover` failed outright on the first deadlock instead of retrying -- confirmed still present by reading current `main`, not just historical. (The lock-ordering root cause of the deadlock itself is a separate, already-fixed story: PR #1150 unified every node-mutating SQL function onto a single LockNodeGroupAndFetch() helper with a consistent lock order, closing that class of AB-BA deadlock -- just without referencing #1004, so it never got linked back to this issue.) Fix: both monitor_perform_failover() and monitor_perform_failover_allow_data_loss() now take a `bool *mayRetry` out-parameter, following the exact convention already used by start/stop_maintenance -- set it and return false when monitor_retryable_error() says the failure is transient, so the caller can retry rather than treating it as fatal. cli_perform.c's cli_perform_failover() (backing both `pg_autoctl perform failover` and `perform switchover`) now wraps the call in the same ConnectionRetryPolicy loop enable/disable maintenance already use. demoapp.c's own call site is updated for the new signature (it already retries on its own schedule via its outer loop, so no behavior change there). Verified: clean build, citus_indent clean, banned.h.sh clean, and a full Docker run of basic_operation.pgaf (27/27, including test_020_multiple_manual_failover_verify_replication_slots which exercises perform_failover directly) confirms no regression to the normal, non-deadlock path.
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.
Fixes #1004 (reopening/re-closing with an actual fix, since the original close had no linked PR or commit).
Background
#1004 reported
pg_autoctl perform switchoverfailing outright on a monitor-side Postgres deadlock:and asked for pg_autoctl to retry transient errors like this rather than fail the whole command.
Three separate things were going on here, and I checked all three against current
main:The deadlock's root cause (inconsistent lock-acquisition order across different node-mutating monitor functions) is already fixed, by pgaftest: DSL/runner improvements + monitor fixes #1150 (
LockNodeGroupAndFetch()/LockNodeGroupAndFetchByName(), unifying every node-mutating entry point onto a single, consistently-ordered lock-then-fetch helper). That PR never referenced Deadlocks during pg_auto_failover operations #1004, so the issue never got closed with an explanation.The retry-policy request is not fixed. pg_autoctl has had a retry mechanism for exactly this SQLSTATE class since 2020 (
monitor_retryable_error()inmonitor.c, coveringdeadlock_detected/serialization_failure/etc, added in Implement a retry policy for deadlocks and other transient failures. #359) --monitor_start_maintenance,monitor_stop_maintenance, andmonitor_register_nodeall use it. Butmonitor_perform_failover()andmonitor_perform_failover_allow_data_loss()-- the exact functions behind the reporter's failing command -- were never wired into it. Confirmed by reading currentmain: both just run their query once and fail hard on any error.The secondary "stuck at demote_timeout forever" symptom the reporter also described (a production failover that left the old primary permanently parked at
demote_timeout, requiring a manual restart) is a different mechanism entirely -- a state-machine deadlock (no legalKeeperFSM[]transition out of an assigned goal state), not a Postgres lock deadlock. That's already fixed too, independently: Fix #1025: self-fenced primary deadlocks forever on an unreachable goal state #1158 (Possible FAILURE STATE in State Machine #1025) and Fix #774: monitor can assign a primary an unreachable goal state during failover #1165 (How to recover from a current/assigned state combination without a possible state transition path? #774) both root-cause and close variants of a primary landing in an unreachable goal state during failover. Not the same bug as 1/2 above, but worth noting since the reporter conflated the two symptoms in the original issue.Fix
monitor_perform_failover()/monitor_perform_failover_allow_data_loss()(monitor.c) now take abool *mayRetryout-parameter, following the exact convention already used bymonitor_start_maintenance/monitor_stop_maintenance: set it and returnfalsewhenmonitor_retryable_error()says the failure is transient.cli_perform_failover()(cli_perform.c, backing bothpg_autoctl perform failoverandperform switchover) wraps the call in the sameConnectionRetryPolicyloopenable/disable maintenancealready use.demoapp.c's own call site updated for the new signature (it already retries on its own schedule via its outer loop; no behavior change there).Verification
make -C src/bin/pg_autoctl).make docker-check(citus_indent) andci/banned.h.shboth clean.basic_operation.pgaf: 27/27 pass, includingtest_020_multiple_manual_failover_verify_replication_slotswhich exercisesperform_failoverdirectly -- confirms no regression to the normal, non-deadlock path.The deadlock itself is timing-sensitive and no longer reliably reproducible now that #1150 fixed the underlying lock-ordering bug, so this is a defense-in-depth fix for the residual case (a transient deadlock/serialization failure from some other, not-yet-identified race) rather than something with its own new regression test that manufactures the exact race.