Skip to content

Recover cluster leader from Postgres failover#280

Merged
jonyoder merged 6 commits into
mainfrom
fix/leader-failover-recovery
Jun 2, 2026
Merged

Recover cluster leader from Postgres failover#280
jonyoder merged 6 commits into
mainfrom
fix/leader-failover-recovery

Conversation

@jonyoder

@jonyoder jonyoder commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Problem

When the Postgres database behind a multi-node PPM cluster fails over (e.g. an AWS RDS maintenance window), PgxLeader can wedge: it keeps emitting pings and considers itself leader, but stops running scheduled work. Two collaborating bugs cause this:

  1. Lead() never returns on persistent failure — its only exit was the stop channel, so the supervisor's leader→follower recovery path was never taken.
  2. The in-memory node map drifts from the store during the outage and verify()'s strict length check then fails permanently, silently skipping every scheduled task.

This is the underlying cause of repeated production incidents (node list length differs ...) downstream in Posit Package Manager (rstudio/package-manager#17889).

Changes

  • clusterIntegrityErr() — a shared cluster-integrity check that reconciles the in-memory node map asymmetrically: prune nodes that have left the store (safe), but never add store-only nodes. Reachable nodes are re-added by handlePingResponse when they answer a ping, which preserves the split-brain guard. verify() becomes a thin wrapper over it.
  • evaluateHealth() + time-based step-down — evaluated on the existing sweep tick. After StepDownTimeout of continuous integrity failure the leader returns from lead(), so Lead() returns nil and the consumer's supervisor re-elects. StepDownTimeout == 0 disables step-down (legacy behavior).
  • Leveled logging — transient mismatches (rolling restarts) log at debug; sustained failure logs at error; the disabled mode stays at debug to avoid per-tick spam.

New config field: PgxLeaderConfig.StepDownTimeout.

Tests

New Postgres-free RecoverySuite covers the asymmetric reconcile (prune vs. error-without-adding), the step-down threshold boundary, reset-on-recovery, and the disabled (0) case. The existing TestLeaderLeadInternal integrity block was updated to the new semantics. Full Postgres-backed suite runs in CI.

jonyoder and others added 3 commits June 2, 2026 11:48
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prune in-memory nodes that have left the store, but never add store-only
nodes (preserving the split-brain guard). Refactor verify() onto the shared
clusterIntegrityErr() and demote its logging to debug.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Evaluate cluster health on the sweep tick; after StepDownTimeout of
continuous failure, return from lead() so the supervisor re-elects. Log
transient mismatches at debug and sustained failures at error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jonyoder jonyoder requested review from glin and jmwoliver June 2, 2026 16:53
evaluateHealth now reconciles the in-memory map against the store on every
sweep tick, so the sweep step must seed the store with the surviving node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread pkg/rselection/impls/pgx/leader.go Outdated
Comment thread pkg/rselection/impls/pgx/leader.go
jonyoder and others added 2 commits June 2, 2026 13:30
Per review feedback: surfacing leader membership changes at INFO captures
them in default logs during customer escalations without requiring a log
level change and restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface the onset and recovery of an unhealthy episode once at INFO so they
appear in default logs for customer escalations, while keeping per-tick
detail at debug to avoid noise on routine self-healing mismatches. Sustained
failures still escalate to error (when step-down is enabled).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"errors"
"time"

"gopkg.in/check.v1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be changed or fixed here, but it looks like rstudio/platform-lib still uses gopkg.in/check.v1 in its tests, while PPM has mostly migrated away from that package. I'll add an issue to track migrating off gopkg.in/check.v1 and we can tackle when we have capacity for it.

@CDRayn CDRayn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Approved.

@jonyoder jonyoder merged commit fe7bc3f into main Jun 2, 2026
3 checks passed
@jonyoder jonyoder deleted the fix/leader-failover-recovery branch June 2, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants