Skip to content

[Bug]: Race condition in unschedulable node auto-switchover #11293

Open

Description

@chunngaiau

Is there an existing issue already for this bug?

  • I have searched for an existing issue, and could not find anything. I believe this is a new bug.

I have read the troubleshooting guide

  • I have read the troubleshooting guide and I think this is a new bug.

I am running a supported version of CloudNativePG

  • I have read the troubleshooting guide and I think this is a new bug.

Contact Details

No response

Version

1.30.0

What version of Kubernetes are you using?

1.35

What is your Kubernetes environment?

Cloud: Amazon EKS

How did you install the operator?

YAML manifest

What happened?

The failure mode here is the same as #11114 where a race condition in the switchover logic causes timeline divergence and lost writes on the new primary. I am opening a new issue because although the mechanism of the bug is the same, the trigger is different and is in another part of the code.

Here, the bug is caused by the automatic switchover logic in setPrimaryToSchedulableNode method in replicas.go. Similar to #11114, the new primary is chosen before the old primary is shutdown. Which means transactions can still be committed on the old primary and replicated to other replicas. Those transactions may not be replicated to the selected new primary and are lost when it is promoted. Manual intervention is then required to reconcile the diverged timelines.

This happened to two of our CNPG clusters during a node pool upgrade and analysis on one of them revealed 45 commits was lost. We run our clusters with

spec:
  instances: 3
  postgresql:
    synchronous:
      dataDurability: required
      failoverQuorum: true
      method: any
      number: 1

Given this configuration, we expect the CNPG operator to ensure no data loss or split-brain happens all client-committed transactions are replicated during an automatic failover/switchover, or to delay any failover/switchover if it can't be guaranteed.

Timeline

  1. We have a 3-instance cluster. -1 is the primary, -2 and -3 are the replicas.
  2. Karpenter taints the -1 node with karpenter.sh/disrupted:NoSchedule.
  3. The operator recognises the taint and initiates a switchover to -2.
Current primary is running on unschedulable node, triggering a switchover
  1. -1 shuts down and -2 is promoted to primary. -2's logs:
Acquired primary lease
received promote request
redo done at 3DC/5222C3D8 system usage: CPU: user: 8.78 s, system: 1.64 s, elapsed: 3364.24 s
selected new timeline ID: 4
  1. -3 fails to resume streaming from -2 because it was ahead of -2's forkpoint for the new timeline. -1 ran pg_rewind and successfully resumed streaming from -2, but the rewind silently dropped commits. The follow error logs was repeated on -3:
could not start WAL streaming: ERROR:  requested starting point 3DC/53000000 on timeline 3 is not in this server's history
DETAIL:  This server's history forked from timeline 3 at 3DC/5222C420.

Analysis

Running pg_waldump on -3's disk shows that it was ahead of -2 by 45 commits when -2 was promoted. These commits might be "lost"1 on the new primary:

# pg_waldump -p . --start=3DC/5222C420 -t 3 --stats=record -r Transaction 00000003000003DC00000052
WAL statistics between 3DC/5222C420 and 3DC/522A36F0:
Type                                           N      (%)          Record size      (%)             FPI size      (%)        Combined size      (%)
----                                           -      ---          -----------      ---             --------      ---        -------------      ---
Transaction/COMMIT                            45 (100.00)                 2925 (100.00)                    0 (  0.00)                 2925 (100.00)
                                        --------                      --------                      --------                      --------
Total                                         45                          2925 [100.00%]                   0 [0.00%]                  2925 [100%]

1 It's only a problem if the old primary had acknowledged the successful commit of these transactions back to the client. If the old primary had already terminated the client connections (e.g. fast shutdown), then these commits were never successful from the perspective of the client. However, in smart shutdown mode, the old primary is still able to return success to the clients. Regardless of shutdown mode, there is still a gap between when the new target primary is selected and when the old primary's shutdown is initiated (the former triggers the latter). Unfortunately, I'm unable to confirm if any of these commits were acknowledged back to the client although it is likely. The primary is configured with ANY 1 in its synchronous_standby_names, so if the commits were visible in one of the replicas, it is very likely that the primary had acknowledged the commit back to the client. Refer to the comment for an explanation of Postgres replication details.

Cluster resource

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

  1. dosubot commented on Aug 4, 2026

    @dosubot

    Your analysis is spot on, and this is indeed the same class of race condition as #11114 but triggered via a different code path.

    Looking at the code, setPrimaryOnSchedulableNode selects a candidate and immediately calls setPrimaryInstance() to designate the new target — without first shutting down or demoting the old primary. This is the same pattern as updatePrimaryPod in the upgrade path.

    What makes this particularly concerning is that the normal failover path in reconcileTargetPrimaryForNonReplicaCluster is actually safer — it first sets a PendingFailoverMarker, marks the old primary as unhealthy, waits for WAL receivers to go down, and only then selects and promotes the new primary [1]. setPrimaryOnSchedulableNode skips all of these safety steps and goes straight to promotion.

    With method: any, number: 1, PostgreSQL can acknowledge commits to either replica. So the 45 commits that landed on -3 but not -2 between target selection and primary shutdown are exactly the expected failure mode of this race.

    Mitigations available today (same as #11114):

    • method: first makes the synchronous standby deterministic, significantly reducing (but not fully eliminating) the race window [2]
    • primaryUpdateMethod: restart avoids the switchover path entirely, though it doesn't help here since the trigger is node drain, not a config update [2]

    v1.30 Primary Lease: The primary lease mechanism merged in v1.30 adds a mutex at promotion time — the new primary must acquire the lease before promoting, and the old primary must release it during shutdown. You mention you're on v1.30 already, so the lease should be active. However, the lease prevents dual-primary but doesn't fully close the window where setPrimaryOnSchedulableNode selects the target before the old primary stops accepting writes. The fix likely needs the same approach as the failover path: use PendingFailoverMarker → demote old primary → wait for WAL receivers down → then select the best candidate by LSN.

    As @ardentperf noted on #11114, the operator would ideally use the lease to ensure the old primary is confirmed shut down before gathering LSNs and selecting a new primary [3].

    To reply, just mention @dosu.


    Share context across your team and agents. Try Dosu.

    Leave Feedback Learn about cloudnative-pg with Dosu Add Dosu to your team

  2. chunngaiau commented on Aug 4, 2026

    @chunngaiau
    Author

    Happy to contribute a fix to this issue. I've opened a draft PR with a potential fix #11294. Also, credit to #11185 for the inspiration of how to fix the previous issue and bringing both the failover and switchover logic into the same code paths.

    Appreciate any feedback as this is my first contribution to the project.

  3. ardentperf commented on Aug 4, 2026

    @ardentperf
    Contributor

    Postgres's design for sync replication is that it will write a commit record to the local WAL however it does NOT send the acknowledgment to the client if there is a network partition and it cannot replicate the commit record. From the clients perspective, the transactions are in-doubt and uncommitted (even if a local WAL log temporarily has the records, until rewind).

    Split brain isn’t a good way to describe this. There are not divergent timelines of client-committed transaction, there is no loss of data known by the client as committed, and there are not two pods processing & acknowledging writes at the same time. During a network partition, all backends and clients will hang on IPC:SyncRep waits.

    It is expected to see a few COMMIT records in the WAL. Those in-doubt COMMITs will always be written to the WAL, even when the new primary starts after the old shuts down according to the wall clock. Clients never receive messages saying these COMMITs were successful.

    The in-doubt transactions may later become visible if there is no cluster reconfiguration and the existing primary is restarted after resolution of the network partition. On the other hand, the in-doubt transactions should never be visible to any client if a new primary is elected and the old primary is rewound after shutdown. Importantly: the thing that should never happen is in-doubt transactions being visible to a client and then later disappearing due to rewind and cluster reconfiguration.

  4. chunngaiau commented on Aug 5, 2026

    @chunngaiau
    Author

    @ardentperf Thanks for the reply and sorry if I have misunderstood the Postgres mechanics here.

    There are not divergent timelines of client-committed transaction, there is no loss of data known by the client as committed, and there are not two pods processing & acknowledging writes at the same time.

    I agree maybe "split-brain" is the not the right word here as only one of the instances are accepting writes at any time (I am new to the terminology). "Data loss" might also be wrong as the data is still there on the diverged replica. But I think there is a loss of client-committed transactions (transactions for which the client has received an acknowledgement) and not just a loss of in-doubt transactions.

    My understanding is that the commits would've been acknowledged by the primary and its clients as long as it's been replicated to one of the replicas (we are running a 3-instance cluster with ANY 1 synchronous replication), but that may not be the pre-selected new primary. From the client's perspective, it would've received acknowledgement of the commit from the old primary, but after the switchover, the commit isn't visible anymore on the new primary.

    The issue here is that the new primary is selected while the old primary is still accepting writes. The old primary can accept COMMITs, write that to WAL, wait until it is acknowledged by any replica and return success to the client between when the new primary was selected and when it is finally promoted. In this period, there is no guarantee that all acknowledged commits are replicated to the pre-selected new primary. In fact, the pre-selected new primary could have lost connection to the new primary immediately after being selected as the new primary. It's worth noting that in smart and fast shutdown, Postgres will try to flush all remaining WAL to its replicas, but this is not guaranteed and the replicas have to be connected.

    I realise I am only talking theoretically here based on tracing the code and I agree seeing commit records with pg_waldump only indicates a possible but not definitive issue. Unfortunately I don't have clear evidence the commits were indeed acknowledged to the client. I'll see if I can come up with a reproducer example to help with the discussions, although this bug relies on very specific timings of the switchover process, so I'm not sure how feasible it is.

  5. ardentperf commented on Aug 5, 2026

    @ardentperf
    Contributor

    The issue here is that the new primary is selected while the old primary is still accepting writes. The old primary can accept COMMITs, write that to WAL, wait until it is acknowledged by any replica and return success to the client between when the new primary was selected and when it is finally promoted. In this period, there is no guarantee that all acknowledged commits are replicated to the pre-selected new primary. In fact, the pre-selected new primary could have lost connection to the new primary immediately after being selected as the new primary.

    CNPG's quorum-based failover should ensure that whenever a new primary is selected, it contains all COMMITs/transactions that have been acknowledged to clients

    https://cloudnative-pg.io/docs/devel/failover/#failover-quorum-quorum-based-failover

    some example network partition scenarios are at the 20 minute mark in last year's kubecon presentation https://youtu.be/iQUOO3-JRK4?si=_nDfBoJn8jsgRmiJ&t=1200

  6. chunngaiau commented on Aug 7, 2026

    @chunngaiau
    Author

    CNPG's quorum-based failover should ensure that whenever a new primary is selected, it contains all COMMITs/transactions that have been acknowledged to clients

    I agree and I think the failover code you are referring to is quite robust. Thank you for linking the presentation.

    To clarify, there are currently three ways automatic cluster reconfiguration happens:

    1. Failover when the primary is unhealthy. This is robust and there is no issue here.
    2. Switchover to update the primary pod. Referenced in [Bug]: Potential race condition in primary update switchover #11114 bug report.
    3. Switchover to move primary to a schedulable node when the primary node is tainted. Referenced in this bug report.

    All three uses slightly different code paths on the operator. The setPrimaryOnSchedulableNode method referenced in this bug report picks the first (sorted by LSN) replica that is on a schedulable node. It does not check if the number of candidates satisfy the failover quorum. The failover quorum check is only performed in the failover code path in the reconcileTargetPrimaryForNonReplicaCluster method. This was confirmed by the lack of quorum check logs in our cluster during the node pool upgrade which taints the primary node.

    This would be fine in most cases though because the setPrimaryOnSchedulableNode method waits until all replicas are on schedulable nodes before initiating the switchover so the chance of it failing the quorum check is pretty low. Although this isn't the case when there are failed pods.

    Nevertheless, even if the failover quorum was checked, the issue remains. If we trace the failover code in reconcileTargetPrimaryForNonReplicaCluster method, it first sets the target primary to a pending marker, waits for all WAL receivers (and indirectly for the primary) to shut down before picking the new primary with the highest LSN. AFAIK this is the correct sequence of steps, however the switchover code in setPrimaryOnSchedulableNode here (and updatePrimaryPod here from #11114) does not follow these steps. They pick the new primary directly without using the pending marker and it's done when the old primary is still accepting writes. This may lead to a less advanced replica being promoted to a new primary, which we observed in both bug reports.

    I'm not sure if it was an intentional design decision to have separate code paths for failover and switchover. My understanding is that they should use the same mechanics for cluster reconfiguration and hence should share the same code.

  7. chadlangston commented on Aug 10, 2026

    @chadlangston

    Thank you for working on this!

    I experienced this bug recently during routine k8s maintenance.

    I have a five instance cnpg cluster.

    When I cordoned the node running the primary, cnpg promoted one of my replicas to be the new primary before shutting down the existing primary.

    This resulted in incompatible timelines and replica crash loops. The controller restored the original primary with pg_rewind.

    The remaining replicas had to be recreated on new PVs from the new primary.

  8. ardentperf commented on Aug 13, 2026

    @ardentperf
    Contributor

    @chunngaiau thanks for the analysis here. i want to walk through the code more closely to better understand the path with node taints. good questions

    @chadlangston want to be careful about drawing conclusions from wall-clock comparisons or pg_rewind execution. bumping the timeline and running rewind doesn't indicate that any transactions were lost (this can potentially happen in a healthy setup, depending how the orchestration works). the case i most recently saw of a replica crash loop with an incompatible timeline was related to a bug in barman-cloud rather than cnpg itself (notably, it did not involve any lost transactions). the focus should be on client-committed transactions, and kits like jepsen are a good way to test for this since they record client-side data and reconcile it with the database. what were your sync replication settings?

  9. github-actions commented on Nov 9, 2026

    @github-actions
    Contributor

    This issue is stale because it has been open for 60 days with no activity.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions