Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/man/borg-with-lock.1
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "borg-with-lock" "1" "2026-07-21" "" "borg backup tool"
.TH "borg-with-lock" "1" "2026-08-13" "" "borg backup tool"
.SH Name
borg-with-lock \- Runs a user-specified command with the repository lock held.
.SH SYNOPSIS
Expand All @@ -55,9 +55,9 @@ code as Borg\(aqs return code.
.INDENT 0.0
.INDENT 3.5
If you copy a repository with the lock held, the lock will be present in
the copy. Before using Borg on the copy from a different host,
you need to run \fBborg break\-lock\fP on the copied repository, because
Borg is cautious and does not automatically remove stale locks made by a different host.
the copy. Borg removes such a leftover lock automatically, but only after
a safety period of about half an hour, so to use Borg on the copy right
away, first run \fBborg break\-lock\fP on the copied repository.
.UNINDENT
.UNINDENT
.SH OPTIONS
Expand Down
6 changes: 3 additions & 3 deletions docs/usage/with-lock.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,6 @@ code as Borg's return code.
.. note::

If you copy a repository with the lock held, the lock will be present in
the copy. Before using Borg on the copy from a different host,
you need to run ``borg break-lock`` on the copied repository, because
Borg is cautious and does not automatically remove stale locks made by a different host.
the copy. Borg removes such a leftover lock automatically, but only after
a safety period of about half an hour, so to use Borg on the copy right
away, first run ``borg break-lock`` on the copied repository.
6 changes: 3 additions & 3 deletions src/borg/archiver/lock_cmds.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser):
.. note::

If you copy a repository with the lock held, the lock will be present in
the copy. Before using Borg on the copy from a different host,
you need to run ``borg break-lock`` on the copied repository, because
Borg is cautious and does not automatically remove stale locks made by a different host.
the copy. Borg removes such a leftover lock automatically, but only after
a safety period of about half an hour, so to use Borg on the copy right
away, first run ``borg break-lock`` on the copied repository.
"""
)
subparser = ArgumentParser(
Expand Down
7 changes: 6 additions & 1 deletion src/borg/storelocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,12 @@ def _get_locks(self):
return {}
for info in infos:
key = info.name
content = self.store.load(f"locks/{key}")
try:
content = self.store.load(f"locks/{key}")
except ObjectNotFound:
# the lock vanished between our listing and loading it, e.g. it was released
# by its owner or another client killed it as stale - so just ignore it.
continue
lock = json.loads(content.decode("utf-8"))
lock["key"] = key
lock["dt"] = datetime.datetime.fromisoformat(lock["time"])
Expand Down
20 changes: 19 additions & 1 deletion src/borg/testsuite/storelocking_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from borgstore.store import Store
from borgstore.store import ObjectNotFound, Store

from ..storelocking import Lock, NotLocked, LockTimeout

Expand Down Expand Up @@ -156,6 +156,24 @@ def delete(name, *args, **kwargs):
lock.refresh()
assert len(list(lockstore.list("locks"))) == 0 # no new lock left behind

def test_lock_vanished_between_list_and_load(self, lockstore, monkeypatch):
# another client can delete a lock (e.g. kill it as stale, or its owner releases it)
# between our listing and our loading of it - such a lock must be skipped, not crash us.
foreign_key = Lock(lockstore, id=ID1)._create_lock(exclusive=True)

orig_load = lockstore.load

def load_vanished(name, *args, **kwargs):
if name == f"locks/{foreign_key}":
raise ObjectNotFound(name)
return orig_load(name, *args, **kwargs)

monkeypatch.setattr(lockstore, "load", load_vanished)
lock = Lock(lockstore, exclusive=True, id=ID2)
assert foreign_key not in lock._get_locks() # skipped, no exception
lock.acquire() # the vanished exclusive lock must not block us
lock.release()

def test_migrate_lock(self, lockstore):
old_id, new_id = ID1, ID2
assert old_id[1] != new_id[1] # different PIDs (like when doing daemonize())
Expand Down
Loading