Skip to content

Commit 9644a83

Browse files
authored
Recheck annotations when doing attempt_to_acquire_leader on 409 (patroni#3348)
We already doing it in update_leader() code path. However, attempt_to_acquire_leader() is also affected by the same problem when for example we are trying to reacquire the leader lock after K8s API outage in DCS failsafe mode. Ref patroni#3174 Close patroni#3344
1 parent 6fbf496 commit 9644a83

File tree

2 files changed

+60
-5
lines changed

2 files changed

+60
-5
lines changed

patroni/dcs/kubernetes.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,10 @@ def _retry(*args: Any, **kwargs: Any) -> Any:
12361236
return bool(_run_and_handle_exceptions(self._patch_or_create, self.leader_path, annotations,
12371237
kind_resource_version, ips=ips, retry=_retry))
12381238

1239+
@staticmethod
1240+
def _isotime() -> str:
1241+
return datetime.datetime.now(tzutc).isoformat()
1242+
12391243
def update_leader(self, cluster: Cluster, last_lsn: Optional[int],
12401244
slots: Optional[Dict[str, int]] = None, failsafe: Optional[Dict[str, str]] = None) -> bool:
12411245
kind = self._kinds.get(self.leader_path)
@@ -1244,7 +1248,7 @@ def update_leader(self, cluster: Cluster, last_lsn: Optional[int],
12441248
if kind and kind_annotations.get(self._LEADER) != self._name:
12451249
return False
12461250

1247-
now = datetime.datetime.now(tzutc).isoformat()
1251+
now = self._isotime()
12481252
leader_observed_record = kind_annotations or self._leader_observed_record
12491253
annotations = {self._LEADER: self._name, 'ttl': str(self._ttl), 'renewTime': now,
12501254
'acquireTime': leader_observed_record.get('acquireTime') or now,
@@ -1262,7 +1266,7 @@ def update_leader(self, cluster: Cluster, last_lsn: Optional[int],
12621266
return self._update_leader_with_retry(annotations, resource_version, self.__ips)
12631267

12641268
def attempt_to_acquire_leader(self) -> bool:
1265-
now = datetime.datetime.now(tzutc).isoformat()
1269+
now = self._isotime()
12661270
annotations = {self._LEADER: self._name, 'ttl': str(self._ttl),
12671271
'renewTime': now, 'acquireTime': now, 'transitions': '0'}
12681272
if self._leader_observed_record:
@@ -1277,18 +1281,55 @@ def attempt_to_acquire_leader(self) -> bool:
12771281
annotations['acquireTime'] = self._leader_observed_record.get('acquireTime') or now
12781282
annotations['transitions'] = str(transitions)
12791283

1284+
resource_version = self._leader_resource_version
1285+
if resource_version:
1286+
kind = self._kinds.get(self.leader_path)
1287+
# If leader object in cache was updated we should better use fresh resource_version
1288+
if kind and kind.metadata.resource_version != resource_version:
1289+
kind_annotations = kind and kind.metadata.annotations or EMPTY_DICT
1290+
# But, only in case if leader annotations didn't change
1291+
if all(kind_annotations.get(k) == self._leader_observed_record.get(k) for k in annotations.keys()):
1292+
resource_version = kind.metadata.resource_version
1293+
1294+
retry = self._retry.copy()
1295+
1296+
def _retry(*args: Any, **kwargs: Any) -> Any:
1297+
kwargs['_retry'] = retry
1298+
return retry(*args, **kwargs)
1299+
1300+
handle_conflict = False
12801301
try:
12811302
ret = bool(self._patch_or_create(self.leader_path, annotations,
1282-
self._leader_resource_version, retry=self.retry, ips=self.__ips))
1303+
resource_version, retry=_retry, ips=self.__ips))
12831304
except k8s_client.rest.ApiException as e:
1284-
if e.status == 409 and self._leader_resource_version: # Conflict in resource_version
1305+
if e.status == 409 and resource_version: # Conflict in resource_version
12851306
# Terminate watchers, it could be a sign that K8s API is in a failed state
12861307
self._kinds.kill_stream()
12871308
self._pods.kill_stream()
1309+
handle_conflict = True
12881310
ret = False
12891311
except (RetryFailedError, K8sException) as e:
12901312
raise KubernetesError(e)
12911313

1314+
if handle_conflict and retry.ensure_deadline(1):
1315+
# if we are here, that means update failed with 409
1316+
# Try to get the latest version directly from K8s API instead of relying on async cache
1317+
try:
1318+
kind = _retry(self._api.read_namespaced_kind, self.leader_path, self._namespace)
1319+
except (RetryFailedError, K8sException) as e:
1320+
raise KubernetesError(e)
1321+
except Exception as e:
1322+
logger.error('Failed to get the leader object "%s": %r', self.leader_path, e)
1323+
else:
1324+
kind_annotations = kind and kind.metadata.annotations or EMPTY_DICT
1325+
kind_resource_version = kind and kind.metadata.resource_version
1326+
1327+
# We can get 409 because we do at least one retry, and the first update might have succeeded,
1328+
# therefore we will check if annotations on the read object match expectations.
1329+
if kind and kind_resource_version != resource_version and\
1330+
all(kind_annotations.get(k) == v for k, v in annotations.items()):
1331+
ret = True
1332+
12921333
if not ret:
12931334
logger.info('Could not take out TTL lock')
12941335
return ret

tests/test_kubernetes.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,13 +294,27 @@ def test_get_citus_coordinator(self, mock_logger):
294294
self.assertEqual(mock_logger.call_args[0][1], 'Citus')
295295
self.assertIsInstance(mock_logger.call_args[0][2], KubernetesError)
296296

297-
def test_attempt_to_acquire_leader(self):
297+
@patch.object(k8s_client.CoreV1Api, 'read_namespaced_config_map', create=True)
298+
def test_attempt_to_acquire_leader(self, mock_read):
299+
metadata = k8s_client.V1ObjectMeta(resource_version='2', labels={'f': 'b'}, name='test',
300+
annotations={'optime': '1234', 'leader': 'p-0', 'transitions': '0',
301+
'renewTime': 'now', 'acquireTime': 'now', 'ttl': '30'})
302+
mock_read.return_value = k8s_client.V1ConfigMap(metadata=metadata)
298303
with patch.object(k8s_client.CoreV1Api, 'patch_namespaced_config_map', create=True) as mock_patch:
299304
mock_patch.side_effect = K8sException
300305
self.assertRaises(KubernetesError, self.k.attempt_to_acquire_leader)
306+
301307
mock_patch.side_effect = k8s_client.rest.ApiException(409, '')
308+
self.k._leader_resource_version = '0'
309+
self.k._isotime = Mock(return_value='now')
310+
self.assertTrue(self.k.attempt_to_acquire_leader())
311+
312+
mock_read.side_effect = Exception
302313
self.assertFalse(self.k.attempt_to_acquire_leader())
303314

315+
mock_read.side_effect = RetryFailedError('')
316+
self.assertRaises(KubernetesError, self.k.attempt_to_acquire_leader)
317+
304318
def test_take_leader(self):
305319
self.k.take_leader()
306320
self.k._leader_observed_record['leader'] = 'test'

0 commit comments

Comments
 (0)