diff --git a/src/borg/storelocking.py b/src/borg/storelocking.py index a81759c703..8f9e89106f 100644 --- a/src/borg/storelocking.py +++ b/src/borg/storelocking.py @@ -189,6 +189,9 @@ def acquire(self): return self time.sleep(self.other_locks_go_away_delay) logger.debug("LOCK-ACQUIRE: timeout while waiting for non-exclusive locks to go away.") + # we won't get the exclusive lock, so do not leave our lock behind: + # it would needlessly block other clients until it expired as stale. + self._delete_lock(key, ignore_not_found=True, update_last_refresh=True) break # timeout else: logger.debug("LOCK-ACQUIRE: someone else also created an exclusive lock, deleting ours.") diff --git a/src/borg/testsuite/storelocking_test.py b/src/borg/testsuite/storelocking_test.py index 8667d79348..361bccdff3 100644 --- a/src/borg/testsuite/storelocking_test.py +++ b/src/borg/testsuite/storelocking_test.py @@ -45,6 +45,17 @@ def test_exclusive_lock(self, lockstore): with pytest.raises(LockTimeout): Lock(lockstore, exclusive=True, id=ID2).acquire() + def test_exclusive_lock_timeout_leaves_no_lock(self, lockstore): + # When acquiring an exclusive lock times out because a non-exclusive lock does not go away, + # the not-acquired exclusive lock must not stay behind in the store: it would block all + # other clients (even on other hosts) until it expired as stale. + with Lock(lockstore, exclusive=False, id=ID1) as shared_lock: + with pytest.raises(LockTimeout): + Lock(lockstore, exclusive=True, id=ID2).acquire() + locks = shared_lock._get_locks() + assert len(locks) == 1 # only the non-exclusive lock of ID1 is left + assert not any(lock["exclusive"] for lock in locks.values()) + def test_double_nonexclusive_lock_succeeds(self, lockstore): with Lock(lockstore, exclusive=False, id=ID1): with Lock(lockstore, exclusive=False, id=ID2):