From 1c2bf258d67a42ea6949b15265082295bed23063 Mon Sep 17 00:00:00 2001 From: Alexander Kukushkin Date: Mon, 4 Oct 2021 16:23:45 +0200 Subject: [PATCH] Allow switchover only to sync nodes when synchronous replication is on (#2076) Close https://github.com/zalando/patroni/issues/2074 --- patroni/ha.py | 12 ++++++++---- tests/test_ha.py | 5 +++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/patroni/ha.py b/patroni/ha.py index 793c5d4f..0ebb8508 100644 --- a/patroni/ha.py +++ b/patroni/ha.py @@ -317,7 +317,7 @@ class Ha(object): if timeout == 0: # We are requested to prefer failing over to restarting master. But see first if there # is anyone to fail over to. - if self.is_failover_possible(self.cluster.members, True): + if self.is_failover_possible(self.cluster.members): logger.info("Master crashed. Failing over.") self.demote('immediate') return 'stopped PostgreSQL to fail over after a crash' @@ -782,6 +782,10 @@ class Ha(object): return False if self.cluster.failover: + # When doing a switchover in synchronous mode only synchronous nodes and former leader are allowed to race + if self.is_synchronous_mode() and self.cluster.failover.leader and \ + self.cluster.failover.candidate and not self.cluster.sync.matches(self.state_handler.name): + return False return self.manual_failover_process_no_leader() if not self.watchdog.is_healthy: @@ -925,7 +929,7 @@ class Ha(object): else: members = [m for m in self.cluster.members if not failover.candidate or m.name == failover.candidate] - if self.is_failover_possible(members): # check that there are healthy members + if self.is_failover_possible(members, False): # check that there are healthy members ret = self._async_executor.try_run_async('manual failover: demote', self.demote, ('graceful',)) return ret or 'manual failover: demoting myself' else: @@ -1179,7 +1183,7 @@ class Ha(object): if self.has_lock() and self.update_lock(): if self._async_executor.scheduled_action == 'doing crash recovery in a single user mode': time_left = self.patroni.config['master_start_timeout'] - (time.time() - self._crash_recovery_started) - if time_left <= 0 and self.is_failover_possible(self.cluster.members, True): + if time_left <= 0 and self.is_failover_possible(self.cluster.members): logger.info("Demoting self because crash recovery is taking too long") self.state_handler.cancellable.cancel(True) self.demote('immediate') @@ -1286,7 +1290,7 @@ class Ha(object): time_left = timeout - self.state_handler.time_in_state() if time_left <= 0: - if self.is_failover_possible(self.cluster.members, True): + if self.is_failover_possible(self.cluster.members): logger.info("Demoting self because master startup is taking too long") self.demote('immediate') return 'stopped PostgreSQL because of startup timeout' diff --git a/tests/test_ha.py b/tests/test_ha.py index 0f30a110..5c198e4a 100644 --- a/tests/test_ha.py +++ b/tests/test_ha.py @@ -624,6 +624,11 @@ class TestHa(PostgresInit): # same as previous, but set the current member to nofailover. In no case it should be elected as a leader self.ha.patroni.nofailover = True self.assertEqual(self.ha.run_cycle(), 'following a different leader because I am not allowed to promote') + # in sync mode only the sync node is allowed to take over + self.ha.cluster = get_cluster_initialized_without_leader(failover=Failover(0, 'leader', 'other', None)) + self.ha.patroni.nofailover = False + self.ha.is_synchronous_mode = true + self.assertEqual(self.ha.run_cycle(), 'following a different leader because i am not the healthiest node') def test_manual_failover_process_no_leader_in_pause(self): self.ha.is_paused = true