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.
This commit is contained in:
Alexander Kukushkin
2023-09-26 11:16:52 +02:00
parent 5ceba81269
commit 4148e0b5b2
3 changed files with 25 additions and 16 deletions
+2 -1
View File
@@ -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
+22 -15
View File
@@ -393,21 +393,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:
+1
View File
@@ -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