Skip to content

Commit a9459f5

Browse files
committed
Fix race condition causing a spurious promote during a global DCS outage
Fallback leader observation mechanism was using a non-quorum read that can see a stale value of multisite status. For purposes of rewinding stadbys this is fine, but if timing was wrong it caused the main HA loop to observe the stale value and promote. Fix this by only running the leader observation fallback while the node is not a leader. If the node is leader the regular heartbeat will take care of updating the view. Observing a stale value during startup is not a problem because promoting to local leader will force a write to global DCS via resolve_leader().
1 parent 10c1bc4 commit a9459f5

2 files changed

Lines changed: 13 additions & 1 deletion

File tree

patroni/ha.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ def set_is_leader(self, value: bool) -> None:
300300
301301
:param value: is the current node the leader.
302302
"""
303+
self.patroni.multisite.set_is_local_leader(value)
303304
with self._leader_expiry_lock:
304305
self._leader_expiry = time.time() + self.dcs.ttl if value else 0
305306
if not value:

patroni/multisite.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ def status(self) -> Dict[str, Any]:
6464
def should_failover(self) -> bool:
6565
return False
6666

67+
def set_is_local_leader(self, value: bool):
68+
"""Update multisite mechanisms view if this node is running as a site leader."""
69+
pass
70+
6771
def on_shutdown(self, checkpoint_location: int, prev_location: int):
6872
pass
6973

@@ -111,6 +115,8 @@ def __init__(self, config: 'Config', on_change: Optional[Callable[..., None]] =
111115

112116
self._dcs_error = None
113117

118+
self._is_local_leader = False
119+
114120
@staticmethod
115121
def get_dcs_config(config: 'Config') -> Tuple[Dict[str, Any], AbstractDCS]:
116122
msconfig = config['multisite']
@@ -419,6 +425,10 @@ def touch_member(self):
419425
logger.info(f"Touching member {self.name} with {data!r}")
420426
self.dcs.touch_member(data)
421427

428+
def set_is_local_leader(self, value: bool):
429+
# Assumes setting a boolean flag from other threads without a lock is atomic
430+
self._is_local_leader = value
431+
422432
def run(self):
423433
self._observe_leader()
424434
while not self._heartbeat.wait(self.config['observe_interval']):
@@ -431,7 +441,8 @@ def run(self):
431441
if self._state_updater:
432442
self._state_updater.store_updates()
433443
while not self._heartbeat.wait(self.config['observe_interval']):
434-
self._observe_leader()
444+
if not self._is_local_leader:
445+
self._observe_leader()
435446

436447
def shutdown(self):
437448
self.stop_requested = True

0 commit comments

Comments
 (0)