Skip to content

fix(instance-manager): promote a replica without waiting for WAL receivers to stop - #11367

Open
leonardoce wants to merge 9 commits into
cloudnative-pg:mainfrom
leonardoce:no-wal-receiver-for-coordination
Open

leonardoce wants to merge 9 commits into
cloudnative-pg:mainfrom
leonardoce:no-wal-receiver-for-coordination

Conversation

@leonardoce

Copy link
Copy Markdown
Contributor

The primary lease now guarantees only one instance can act as primary
at a time, so failovers and switchovers no longer need to stall until
every WAL receiver in the cluster reports itself down before a new
primary is chosen and promoted. This removes a source of delay and
stuck-waiting states during failover.

… failover

When a failover is in progress and the old primary's PostgreSQL is
unresponsive, skip the checkpoint and fast-shutdown attempts and shut it
down immediately instead, so demoting the old primary doesn't stall
waiting on timeouts and the failover can complete without delay.

Signed-off-by: Leonardo Cecchi <[email protected]>
tryTakeOver read the lease with the caller's un-deadlined context, so a
Get against an unreachable API server could block indefinitely instead
of retrying at RetryPeriod cadence. That silence starves the lease
watchdog's heartbeat, which can fence a primary that is still correctly
retrying and has not actually stepped down.

Wrap the call in a RetryPeriod-bounded context, mirroring the same
pattern already used for the post-renewal lease check.

Closes cloudnative-pg#11349

Signed-off-by: Leonardo Cecchi <[email protected]>
…ease

Primary self-fencing on network isolation used to live in the liveness
probe: on API-server unreachability it pinged every peer's failsafe
endpoint, and a failed probe caused Kubelet to restart the pod roughly
three probe periods (~30s) after a partition started.

The primary lease now gives a faster, more precise signal for the same
condition. When the primary fails to renew its lease, it runs the same
peer-reachability check itself and, if it should step down, requests a
fast PostgreSQL shutdown directly rather than waiting on Kubelet.

The check runs as soon as renewal fails (not after waiting out the
remaining lease TTL): the RenewDeadline-to-LeaseDuration margin is the
time budget for the check-and-shutdown sequence to complete before a
replica becomes eligible to promote, not idle slack to wait through.

The step-down condition is no longer just peer unreachability: the
/failsafe entrypoint now reports the responding instance's view of the
cluster's target primary (via a new internal/management/failsafe wire
package shared by both sides), so a primary that can still reach every
peer but learns from one of them that someone else is now the target
primary steps down too. This closes a gap the reachability check alone
couldn't see: a peer being reachable doesn't mean it hasn't already
been promoted.

The liveness probe's isolation logic is removed now that the lease
handles it; IsHealthy always reports OK.

Signed-off-by: Leonardo Cecchi <[email protected]>
The liveness probe used to always report healthy, so a Pod could never
be restarted when it truly got stuck. It now fails, so the kubelet can
kill and recreate the Pod, in two cases the primary lease and fencing
logic can't recover from on their own:

- the primary-lease loop stops attempting to renew or take over the
  lease altogether (e.g. a deadlock), as opposed to merely failing to
  renew, which is an expected condition handled separately by the
  lease/failsafe step-down check;

- PostgreSQL was asked to shut down immediately (e.g. because this
  primary had to step down) but didn't honor it within a new
  configurable `.spec.immediateShutdownTimeout` (default 30s).

Signed-off-by: Leonardo Cecchi <[email protected]>
@cnpg-bot cnpg-bot added backport-requested ◀️ This pull request should be backported to all supported releases release-1.28 release-1.29 release-1.30 labels Aug 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

❗ By default, the pull request is configured to backport to all release branches.

  • To stop backporting this pr, remove the label: backport-requested ◀️ or add the label 'do not backport'
  • To stop backporting this pr to a certain release branch, remove the specific branch label: release-x.y

@leonardoce leonardoce added do not backport This PR must not be backported - it will be in the next minor release no-issue and removed backport-requested ◀️ This pull request should be backported to all supported releases release-1.28 release-1.29 release-1.30 labels Aug 24, 2026
@leonardoce

Copy link
Copy Markdown
Contributor Author

IMPORTANT: this requires #11347 #11350 #11357 and #11358 to be merged before a real review. They are cherry-picked here out of convenience.

@leonardoce

Copy link
Copy Markdown
Contributor Author

/test

@github-actions

Copy link
Copy Markdown
Contributor

@leonardoce, here's the link to the E2E on CNPG workflow run: https://github.com/cloudnative-pg/cloudnative-pg/actions/runs/32732041476

@leonardoce

leonardoce commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor Author

This is what happened:

  • 14:44:50.109 — Test corrupts the primary (-1) by deleting its PGDATA while postgres keeps running
  • 14:44:5x – 14:45:17 — -1's postmaster starts returning FATAL: "base/5" is not a valid data directory for new connections/backends, but its pre-existing walsender connections to -2 and -3 (established before the corruption) keep running normally — they only read WAL segment files and don't need to touch the corrupted catalog
  • 14:45:17.847 — Operator (decides the new primary: it snapshots replica status and finds -2 and -3 tied at receivedLsn = replayLsn = 0/6055A20; it picks -2 and sets cluster.Status.TargetPrimary = -2
  • 14:45:18.276 – 18.406 — -1's own instance-manager notices it's no longer TargetPrimary, requests a fast shutdown; postgres on -1 (and both its walsenders) dies
  • 14:45:18.397 / .401 — -2's and -3's walreceivers both disconnect essentially simultaneously (SSL EOF) — i.e. both were still actively streaming, at slightly different effective positions, right up to the moment -1 actually died
  • 14:45:18.563 — -1 releases the primary lease
  • 14:45:20.495 – 21.562 — -2 acquires the lease (fast path: empty holder, no 15s TTL wait needed) and promotes — ~2.6s after the original election snapshot, and ~1.9s after the lease was actually released
  • (at promotion) — -2's fork point is 0/6055A20 (its replay had only reached there) — but -3 had already received WAL up to 0/7000000 in the meantime
  • 14:45:5x → 14:51:09 (timeout) — -3 endlessly retries streaming from 0/7000000, failing forever with requested starting point 0/7000000 on timeline 1 is not in this server's history — it diverged onto WAL the new primary never had, and nothing detects/repairs this (no automatic re-clone/rewind)

So the primary lease worked correctly, but unfortunately the operator decided which replica to promote too early, without re-checking it before the promotion actually happens.

Claude was used to help wording the timeline.

While a failover was pending (target primary set to the internal
"pending" marker), the reconciliation loop kept short-circuiting with
a 1-second requeue instead of continuing to check whether the
failover could complete, and the status update logic would mistake
the marker for a missing pod and reset the target primary back to the
unhealthy one. Both effects could leave a failover stuck indefinitely
or silently undone. The reconciler now recognizes the pending marker
and lets the failover proceed to completion.

Signed-off-by: Leonardo Cecchi <[email protected]>
…ivers to stop

The primary lease now guarantees only one instance can act as primary
at a time, so failovers and switchovers no longer need to stall until
every WAL receiver in the cluster reports itself down before a new
primary is chosen and promoted. This removes a source of delay and
stuck-waiting states during failover.

Signed-off-by: Leonardo Cecchi <[email protected]>
The controller used to snapshot replica LSNs and immediately elect a
new primary in the same reconcile pass as marking the failover
pending, even though the old primary could still be alive and
streaming for a few more seconds. That window let the LSN snapshot go
stale before the elected replica actually promoted, leaving a sibling
replica to fork onto a diverged timeline with no automatic repair.

Splits "mark pending" and "elect" into two separate reconcile passes,
deferring election until the primary lease is confirmed released or
expired, so the LSN snapshot used for the election is taken as close
as possible to the actual promotion.

Signed-off-by: Leonardo Cecchi <[email protected]>
…former primary

When an unreachable former primary has no running postmaster, requesting
a shutdown has nothing to act on it and can block forever on the
unbuffered command channel. Skip the request in that case, and export
IsStatusRunning so the controller can check it.

Signed-off-by: Leonardo Cecchi <[email protected]>
@leonardoce
leonardoce force-pushed the no-wal-receiver-for-coordination branch from f1ad3f1 to 57f3b1d Compare August 25, 2026 13:07
@leonardoce

Copy link
Copy Markdown
Contributor Author

/test

@github-actions

Copy link
Copy Markdown
Contributor

@leonardoce, here's the link to the E2E on CNPG workflow run: https://github.com/cloudnative-pg/cloudnative-pg/actions/runs/32855976902

@leonardoce

Copy link
Copy Markdown
Contributor Author
  • 15:52:08.88 → 15:52:23.17: 8 consecutive "Waiting for the primary lease to be released or expire" reconciles, 1 every ~2s (the RequeueAfter: 1 * time.Second from handleSwitchover's ErrPrimaryLeaseHeld branch, though logs land ~2s apart)
  • 15:52:25.26: FailoverTarget event finally fires — ~16.4s after the failover was first marked pending

It looks like the tests time budget is not enough to observe a failover. That's even more important now given that, in these conditions, we need to wait for the lease to expire.

Signed-off-by: Leonardo Cecchi <[email protected]>
@leonardoce

Copy link
Copy Markdown
Contributor Author

/test

@github-actions

Copy link
Copy Markdown
Contributor

@leonardoce, here's the link to the E2E on CNPG workflow run: https://github.com/cloudnative-pg/cloudnative-pg/actions/runs/32943483652

@cnpg-bot cnpg-bot added the ok to merge 👌 This PR can be merged label Aug 26, 2026
@leonardoce
leonardoce marked this pull request as ready for review August 26, 2026 11:10
@leonardoce
leonardoce requested review from a team, NiccoloFei and litaocdl as code owners August 26, 2026 11:10
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. release-1.28 release-1.29 release-1.30 labels Aug 26, 2026
@gbartolini

Copy link
Copy Markdown
Contributor

@leonardoce could this be related to #11114?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not backport This PR must not be backported - it will be in the next minor release no-issue ok to merge 👌 This PR can be merged size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants