Skip to content

Commit 0485f2e

Browse files
authored
Add renew method (#61)
* Add `renew` method to `Lock` classes * Add intergration tests * Fix linting errors
1 parent aa22503 commit 0485f2e

File tree

2 files changed

+323
-0
lines changed

2 files changed

+323
-0
lines changed

sherlock/lock.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,22 @@ def release(self):
206206

207207
return self._release()
208208

209+
def _renew(self) -> bool:
210+
"""
211+
Implementation of renewing an acquired lock. Must be implemented in
212+
the sub-class.
213+
"""
214+
raise NotImplementedError("Must be implemented in the sub-class")
215+
216+
def renew(self) -> bool:
217+
"""
218+
Renew a lock that is already acquired.
219+
"""
220+
return self._renew()
221+
209222
def __enter__(self):
210223
self.acquire()
224+
return self
211225

212226
def __exit__(self, exc_type, exc_value, traceback):
213227
self.release()
@@ -330,6 +344,15 @@ def _release(self):
330344
)
331345
return self._lock_proxy.release()
332346

347+
def _renew(self) -> bool:
348+
if self._lock_proxy is None:
349+
raise LockException(
350+
"Lock backend has not been configured and "
351+
"lock cannot be acquired or released. "
352+
"Configure lock backend first."
353+
)
354+
return self._lock_proxy.renew()
355+
333356
@property
334357
def _locked(self):
335358
if self._lock_proxy is None:
@@ -405,6 +428,19 @@ class RedisLock(BaseLock):
405428
return result
406429
"""
407430

431+
_renew_script = """
432+
local result = 0
433+
if redis.call('GET', KEYS[1]) == KEYS[2] then
434+
if KEYS[3] ~= -1 then
435+
redis.call('EXPIRE', KEYS[1], KEYS[3])
436+
else
437+
redis.call('PERSIST', KEYS[1])
438+
end
439+
result = 1
440+
end
441+
return result
442+
"""
443+
408444
def __init__(self, lock_name, **kwargs):
409445
"""
410446
:param str lock_name: name of the lock to uniquely identify the lock
@@ -433,6 +469,7 @@ def __init__(self, lock_name, **kwargs):
433469
# Register Lua script
434470
self._acquire_func = self.client.register_script(self._acquire_script)
435471
self._release_func = self.client.register_script(self._release_script)
472+
self._renew_func = self.client.register_script(self._renew_script)
436473

437474
@property
438475
def _key_name(self):
@@ -465,6 +502,14 @@ def _release(self):
465502

466503
self._owner = None
467504

505+
def _renew(self) -> bool:
506+
if self._owner is None:
507+
raise LockException("Lock was not set by this process.")
508+
509+
if self._release_func(keys=[self._key_name, self._owner, self.expire]) != 1:
510+
return False
511+
return True
512+
468513
@property
469514
def _locked(self):
470515
if self.client.get(self._key_name) is None:
@@ -579,6 +624,23 @@ def _release(self):
579624
"Lock could not be released as it has not been acquired"
580625
)
581626

627+
def _renew(self) -> bool:
628+
if self._owner is None:
629+
raise LockException("Lock was not set by this process.")
630+
631+
try:
632+
self.client.write(
633+
self._key_name,
634+
None,
635+
ttl=self.expire,
636+
prevValue=self._owner,
637+
prevExist=True,
638+
refresh=True,
639+
)
640+
return True
641+
except (etcd.EtcdCompareFailed, etcd.EtcdKeyNotFound):
642+
return False
643+
582644
@property
583645
def _locked(self):
584646
try:
@@ -702,6 +764,21 @@ def _release(self):
702764
"Lock could not be released as it has not " "been acquired"
703765
)
704766

767+
def _renew(self) -> bool:
768+
if self._owner is None:
769+
raise LockException("Lock was not set by this process.")
770+
771+
resp = self.client.get(self._key_name)
772+
if resp is not None:
773+
if resp == str(self._owner):
774+
_args = [self._key_name, self._owner]
775+
if self.expire is not None:
776+
_args.append(self.expire)
777+
# Update key with new TTL
778+
self.client.set(*_args)
779+
return True
780+
return False
781+
705782
@property
706783
def _locked(self):
707784
return True if self.client.get(self._key_name) is not None else False
@@ -953,6 +1030,32 @@ def _release(self) -> None:
9531030
# protects us from race conditions in deleting the Lease.
9541031
self._delete_lease(lease)
9551032

1033+
def _renew(self) -> bool:
1034+
if self._owner is None:
1035+
raise LockException("Lock was not set by this process.")
1036+
1037+
lease = self._get_lease()
1038+
if lease is not None and self._owner == lease.spec.holder_identity:
1039+
now = self._now()
1040+
has_expired = self._has_expired(lease, now)
1041+
1042+
if has_expired:
1043+
return False
1044+
1045+
lease.spec.acquire_time = now
1046+
lease.spec.renew_time = now
1047+
lease.spec.lease_duration_seconds = self.expire
1048+
# The Lease object contains a `.metadata.resource_version` which
1049+
# protects us from race conditions in updating the Lease as described:
1050+
# https://blog.atomist.com/kubernetes-apply-replace-patch/.
1051+
if self._replace_lease(lease) is None:
1052+
# Someone else has modified the Lease before we renewed it so it
1053+
# must've expired.
1054+
raise False
1055+
return True
1056+
1057+
return False
1058+
9561059
@property
9571060
def _locked(self):
9581061
lease = self._get_lease()
@@ -1107,6 +1210,25 @@ def _release(self) -> None:
11071210
if self._owner == data["owner"]:
11081211
self._data_file.unlink()
11091212

1213+
def _renew(self) -> bool:
1214+
if self._owner is None:
1215+
raise LockException("Lock was not set by this process.")
1216+
1217+
if self._data_file.exists():
1218+
with self._lock_file:
1219+
data = json.loads(self._data_file.read_text())
1220+
1221+
now = self._now()
1222+
has_expired = self._has_expired(data, now)
1223+
if self._owner == data["owner"]:
1224+
if has_expired:
1225+
return False
1226+
# Refresh expiry time.
1227+
data["expiry_time"] = self._expiry_time()
1228+
self._data_file.write_text(json.dumps(data))
1229+
return True
1230+
return False
1231+
11101232
@property
11111233
def _locked(self):
11121234
if not self._data_file.exists():

0 commit comments

Comments
 (0)