fix(update): an adopted own-pid claim is refreshed, not kept near expiry
Adopting a killed update's marker verbatim kept its started_at. When that claim was close to the 20-minute ceiling, the retry's own lock expired seconds into its run and a second updater could start over the same checkout: live, a detached `hermes update` acquired the lock 6s after the retry adopted a claim 3s from expiry. A marker naming our pid can only be a dead predecessor's (nothing pre-writes one for `hermes update`, which always runs under a parent's claim), so it is a new attempt and is claimed fresh like any dead holder's. Only one acquire runs per process, so this cannot renew a wedged update.
This commit is contained in:
@@ -165,14 +165,12 @@ class UpdateLock:
|
|||||||
release. The ancestry path covers staged updaters older than the env-var export.
|
release. The ancestry path covers staged updaters older than the env-var export.
|
||||||
"""
|
"""
|
||||||
existing = read_live_update(path=self.path)
|
existing = read_live_update(path=self.path)
|
||||||
if existing is not None:
|
# A live claim naming our own pid is a killed update's marker whose pid this retry
|
||||||
if existing.pid == os.getpid():
|
# inherited (containers restart pid numbering): no other live process has our pid, and
|
||||||
# No other live process has our pid, so this claim is ours: a desktop pre-write, or
|
# nothing pre-writes a marker for `hermes update` (it always runs under a parent's claim).
|
||||||
# the marker of a killed update whose pid this retry inherited (containers restart
|
# It is a new attempt, so it is claimed fresh like a dead holder's. Keeping the old
|
||||||
# pid numbering). Adopt it verbatim like UpdateMarkerGuard::acquire in update.rs —
|
# started_at would let the ceiling expire mid-run and admit a second updater.
|
||||||
# rewriting started_at would let retries keep a wedged update under the age ceiling.
|
if existing is not None and existing.pid != os.getpid():
|
||||||
self.acquired = True
|
|
||||||
return True
|
|
||||||
if existing.pid == _handoff_pid() or _is_ancestor_pid(existing.pid):
|
if existing.pid == _handoff_pid() or _is_ancestor_pid(existing.pid):
|
||||||
return True
|
return True
|
||||||
self.holder = existing
|
self.holder = existing
|
||||||
|
|||||||
@@ -99,19 +99,23 @@ def test_refused_lock_does_not_delete_the_live_owners_marker(marker, other_pid):
|
|||||||
assert marker.exists(), "a refused claimant must never clear the live owner's lock"
|
assert marker.exists(), "a refused claimant must never clear the live owner's lock"
|
||||||
|
|
||||||
|
|
||||||
def test_marker_naming_our_own_pid_is_adopted(marker):
|
def test_marker_naming_our_own_pid_is_adopted(marker, monkeypatch):
|
||||||
"""A killed update's marker names the pid its retry gets (containers restart pid numbering).
|
"""A killed update's marker names the pid its retry gets (containers restart pid numbering).
|
||||||
|
|
||||||
No other live process can hold our pid, so the claim is ours: adopt it verbatim (like
|
No other live process can hold our pid, so the claim is ours: take it instead of refusing
|
||||||
``UpdateMarkerGuard::acquire``) instead of refusing "another update" for up to 20 minutes.
|
"another update" for up to 20 minutes. It is a new attempt, so it is claimed fresh: a
|
||||||
|
nearly-expired claim must still block a second updater for our whole run.
|
||||||
"""
|
"""
|
||||||
started_at = int(time.time()) - 60
|
_claim(marker, os.getpid(), time.time() - UPDATE_MARKER_MAX_AGE_SECONDS + 5)
|
||||||
_claim(marker, os.getpid(), started_at)
|
|
||||||
|
|
||||||
lock = UpdateLock(path=marker)
|
lock = UpdateLock(path=marker)
|
||||||
assert lock.acquire() is True
|
assert lock.acquire() is True
|
||||||
assert lock.acquired is True
|
assert lock.acquired is True
|
||||||
assert marker.read_text(encoding="utf-8").splitlines() == [str(os.getpid()), str(started_at)]
|
|
||||||
|
real_time = time.time
|
||||||
|
monkeypatch.setattr(time, "time", lambda: real_time() + 60)
|
||||||
|
holder = read_live_update(path=marker)
|
||||||
|
assert holder is not None and holder.pid == os.getpid(), "a second updater would start mid-run"
|
||||||
lock.release()
|
lock.release()
|
||||||
assert not marker.exists()
|
assert not marker.exists()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user