From 48514db84b631a69e6191642832be378a0c63a4f Mon Sep 17 00:00:00 2001 From: Alexander Kukushkin Date: Tue, 26 Sep 2023 09:12:31 +0200 Subject: [PATCH] Take into account current role when deciding on removal of member ZNode (#2884) Patroni doesn't watch on all changes of member keys in order to not create too much load on ZooKeeper, but only subscribes to changes (ZNodes added or deleted) in the `/member` directory. Therefore when some important fields in the value are updated we remove and recreate ZNode in order to notify the leader or other members. The leader should remove the member key only when the `checkpoint_after_promote` value is changed and replicas when the `state` is changed to/from `running`. We don't care about the `version` field, because Patroni version can't be changed without restart, what will case ZooKeeper `session_id` to change it anyway. This fix hopefully will reduce failures of behave tests on GH Actions. --- features/patroni_api.feature | 3 ++- patroni/dcs/zookeeper.py | 37 +++++++++++++++++++++--------------- tests/test_zookeeper.py | 1 + 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/features/patroni_api.feature b/features/patroni_api.feature index 624d3271..ff06f3c9 100644 --- a/features/patroni_api.feature +++ b/features/patroni_api.feature @@ -68,6 +68,7 @@ Scenario: check API requests for the primary-replica pair in the pause mode When I kill postmaster on postgres1 And I issue a GET request to http://127.0.0.1:8009/replica Then I receive a response code 503 + And "members/postgres1" key in DCS has state=stopped after 10 seconds When I run patronictl.py restart batman postgres1 --force Then I receive a response returncode 0 Then replication works from postgres0 to postgres1 after 20 seconds @@ -76,7 +77,7 @@ Scenario: check API requests for the primary-replica pair in the pause mode Then I receive a response code 200 And I receive a response state running And I receive a response role replica - When I run patronictl.py reinit batman postgres1 --force + When I run patronictl.py reinit batman postgres1 --force --wait Then I receive a response returncode 0 And I receive a response output "Success: reinitialize for member postgres1" And postgres1 role is the secondary after 30 seconds diff --git a/patroni/dcs/zookeeper.py b/patroni/dcs/zookeeper.py index 6390aa2e..863d7b40 100644 --- a/patroni/dcs/zookeeper.py +++ b/patroni/dcs/zookeeper.py @@ -379,21 +379,28 @@ class ZooKeeper(AbstractDCS): cluster = self.cluster member = cluster and cluster.get_member(self._name, fallback_to_leader=False) member_data = self.__last_member_data or member and member.data - # We want to notify leader if some important fields in the member key changed by removing ZNode - if member and (self._client.client_id is not None and member.session != self._client.client_id[0] - or not (member_data and deep_compare(member_data.get('tags', {}), data.get('tags', {})) - and (member_data.get('state') == data.get('state') - or 'running' not in (member_data.get('state'), data.get('state'))) - and member_data.get('version') == data.get('version') - and member_data.get('checkpoint_after_promote') - == data.get('checkpoint_after_promote'))): - try: - self._client.delete_async(self.member_path).get(timeout=1) - except NoNodeError: - pass - except Exception: - return False - member = None + if member and member_data: + is_leader = data.get('role') in ('master', 'primary', 'standby_leader') + checkpoint_after_promote_changed = member_data.get('checkpoint_after_promote') \ + != data.get('checkpoint_after_promote') + state_running_changed = member_data.get('state') != data.get('state') \ + and 'running' in (member_data.get('state'), data.get('state')) + tags_changed = not deep_compare(member_data.get('tags', {}), data.get('tags', {})) + + # We want delete the member ZNode if: + # - our session doesn't match with session id on our member key; or + # - we want to notify leader if some important fields in the member key changed; or + # - if we are the leader and want to notify replicas about checkpoint_after_promote; + if self._client.client_id is not None and member.session != self._client.client_id[0] \ + or is_leader and checkpoint_after_promote_changed \ + or not is_leader and (state_running_changed or tags_changed): + try: + self._client.delete_async(self.member_path).get(timeout=1) + except NoNodeError: + pass + except Exception: + return False + member = None encoded_data = json.dumps(data, separators=(',', ':')).encode('utf-8') if member and member_data: diff --git a/tests/test_zookeeper.py b/tests/test_zookeeper.py index 45aeac10..184fda05 100644 --- a/tests/test_zookeeper.py +++ b/tests/test_zookeeper.py @@ -276,6 +276,7 @@ class TestZooKeeper(unittest.TestCase): self.assertTrue(self.zk.delete_cluster()) def test_watch(self): + self.zk.event.wait = Mock() self.zk.watch(None, 0) self.zk.event.is_set = Mock(return_value=True) self.zk._fetch_status = False