Fix bug with priority failover (#3297)

We should ignor the former leader with higher priority when it reports the same LSN as the current node.

This bug could be a contributing factor to issues described in #3295


In addition to that mock socket.getaddrinfo() call in test_api.py to avoid hitting DNS servers.
This commit is contained in:
Alexander Kukushkin
2025-02-28 09:48:16 +01:00
committed by GitHub
parent 92c4f9fbb5
commit a316105412
3 changed files with 21 additions and 2 deletions
+16 -2
View File
@@ -1250,12 +1250,15 @@ class Ha(object):
lag = self.cluster.status.last_lsn - wal_position lag = self.cluster.status.last_lsn - wal_position
return lag > global_config.maximum_lag_on_failover return lag > global_config.maximum_lag_on_failover
def _is_healthiest_node(self, members: Collection[Member], check_replication_lag: bool = True) -> bool: def _is_healthiest_node(self, members: Collection[Member],
check_replication_lag: bool = True,
leader: Optional[Leader] = None) -> bool:
"""Determine whether the current node is healthy enough to become a new leader candidate. """Determine whether the current node is healthy enough to become a new leader candidate.
:param members: the list of nodes to check against :param members: the list of nodes to check against
:param check_replication_lag: whether to take the replication lag into account. :param check_replication_lag: whether to take the replication lag into account.
If the lag exceeds configured threshold the node disqualifies itself. If the lag exceeds configured threshold the node disqualifies itself.
:param leader: the old cluster leader, it will be used to ignore its ``failover_priority`` value.
:returns: ``True`` if the node is eligible to become the new leader. Since this method is executed :returns: ``True`` if the node is eligible to become the new leader. Since this method is executed
on multiple nodes independently it is possible that multiple nodes could count on multiple nodes independently it is possible that multiple nodes could count
themselves as the healthiest because they received/replayed up to the same LSN, themselves as the healthiest because they received/replayed up to the same LSN,
@@ -1297,6 +1300,12 @@ class Ha(object):
quorum_votes = 0 if self.state_handler.name in voting_set else -1 quorum_votes = 0 if self.state_handler.name in voting_set else -1
nodes_ahead = 0 nodes_ahead = 0
# we need to know the name of the former leader to ignore it if it has higher failover_priority
if self.sync_mode_is_active():
leader_name = self.cluster.sync.leader
else:
leader_name = leader and leader.name
for st in self.fetch_nodes_statuses(members): for st in self.fetch_nodes_statuses(members):
if st.failover_limitation() is None: if st.failover_limitation() is None:
if st.in_recovery is False: if st.in_recovery is False:
@@ -1315,6 +1324,11 @@ class Ha(object):
low_priority = my_wal_position == st.wal_position \ low_priority = my_wal_position == st.wal_position \
and self.patroni.failover_priority < st.failover_priority and self.patroni.failover_priority < st.failover_priority
if low_priority and leader_name and leader_name == st.member.name:
logger.info('Ignoring former leader %s having priority %s higher than this nodes %s priority',
leader_name, st.failover_priority, self.patroni.failover_priority)
low_priority = False
if low_priority and (not self.sync_mode_is_active() or quorum_vote): if low_priority and (not self.sync_mode_is_active() or quorum_vote):
# There's a higher priority non-lagging replica # There's a higher priority non-lagging replica
logger.info( logger.info(
@@ -1505,7 +1519,7 @@ class Ha(object):
# run usual health check # run usual health check
members = {m.name: m for m in all_known_members} members = {m.name: m for m in all_known_members}
return self._is_healthiest_node(members.values()) return self._is_healthiest_node(members.values(), leader=self.old_cluster.leader)
def _delete_leader(self, last_lsn: Optional[int] = None) -> None: def _delete_leader(self, last_lsn: Optional[int] = None) -> None:
self.set_is_leader(False) self.set_is_leader(False)
+2
View File
@@ -18,6 +18,7 @@ from patroni.psycopg import OperationalError
from patroni.utils import RetryFailedError, tzutc from patroni.utils import RetryFailedError, tzutc
from . import MockConnect, psycopg_connect from . import MockConnect, psycopg_connect
from .test_etcd import socket_getaddrinfo
from .test_ha import get_cluster_initialized_without_leader from .test_ha import get_cluster_initialized_without_leader
future_restart_time = datetime.datetime.now(tzutc) + datetime.timedelta(days=5) future_restart_time = datetime.datetime.now(tzutc) + datetime.timedelta(days=5)
@@ -711,6 +712,7 @@ class TestRestApiServer(unittest.TestCase):
patch.object(MockRestApiServer, 'server_close', Mock()): patch.object(MockRestApiServer, 'server_close', Mock()):
self.srv.reload_config({'listen': ':8008'}) self.srv.reload_config({'listen': ':8008'})
@patch('socket.getaddrinfo', socket_getaddrinfo)
@patch.object(MockPatroni, 'dcs') @patch.object(MockPatroni, 'dcs')
def test_check_access(self, mock_dcs): def test_check_access(self, mock_dcs):
mock_dcs.cluster = get_cluster_initialized_without_leader() mock_dcs.cluster = get_cluster_initialized_without_leader()
+3
View File
@@ -1076,6 +1076,9 @@ class TestHa(PostgresInit):
# if there is a higher-priority node but it has a lower WAL position then this node should race # if there is a higher-priority node but it has a lower WAL position then this node should race
self.ha.fetch_node_status = get_node_status(failover_priority=6, wal_position=9) self.ha.fetch_node_status = get_node_status(failover_priority=6, wal_position=9)
self.assertTrue(self.ha._is_healthiest_node(self.ha.old_cluster.members)) self.assertTrue(self.ha._is_healthiest_node(self.ha.old_cluster.members))
# if the old leader is a higher-priority node on the same WAL position then this node should race
self.ha.fetch_node_status = get_node_status(failover_priority=6)
self.assertTrue(self.ha._is_healthiest_node(self.ha.old_cluster.members, leader=self.ha.old_cluster.leader))
self.ha.fetch_node_status = get_node_status(wal_position=11) # accessible, in_recovery, wal position ahead self.ha.fetch_node_status = get_node_status(wal_position=11) # accessible, in_recovery, wal position ahead
self.assertFalse(self.ha._is_healthiest_node(self.ha.old_cluster.members)) self.assertFalse(self.ha._is_healthiest_node(self.ha.old_cluster.members))
# in synchronous_mode consider itself healthy if the former leader is accessible in read-only and ahead of us # in synchronous_mode consider itself healthy if the former leader is accessible in read-only and ahead of us