2
0
Эх сурвалжийг харах

with-lock: refresh repo lock while subprocess is running, fixes #8347

otherwise the lock might become stale and could get
killed by any other borg process.

note: ThreadRunner class written by PyCharm AI and
only needed small enhancements. nice.
Thomas Waldmann 9 сар өмнө
parent
commit
60a592d50f

+ 7 - 1
src/borg/archiver/lock_cmds.py

@@ -4,7 +4,7 @@ import subprocess
 from ._common import with_repository
 from ..cache import Cache
 from ..constants import *  # NOQA
-from ..helpers import prepare_subprocess_env, set_ec, CommandError
+from ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner
 
 from ..logger import create_logger
 
@@ -15,6 +15,10 @@ class LocksMixIn:
     @with_repository(manifest=False, exclusive=True)
     def do_with_lock(self, args, repository):
         """run a user specified command with the repository lock held"""
+        # the repository lock needs to get refreshed regularly, or it will be killed as stale.
+        # refreshing the lock is not part of the repository API, so we do it indirectly via repository.info.
+        lock_refreshing_thread = ThreadRunner(sleep_interval=60, target=repository.info)
+        lock_refreshing_thread.start()
         env = prepare_subprocess_env(system=True)
         try:
             # we exit with the return code we get from the subprocess
@@ -22,6 +26,8 @@ class LocksMixIn:
             set_ec(rc)
         except (FileNotFoundError, OSError, ValueError) as e:
             raise CommandError(f"Error while trying to run '{args.command}': {e}")
+        finally:
+            lock_refreshing_thread.terminate()
 
     @with_repository(lock=False, manifest=False)
     def do_break_lock(self, args, repository):

+ 1 - 1
src/borg/helpers/__init__.py

@@ -39,7 +39,7 @@ from .parseformat import BaseFormatter, ArchiveFormatter, ItemFormatter, DiffFor
 from .parseformat import swidth_slice, ellipsis_truncate
 from .parseformat import BorgJsonEncoder, basic_json_data, json_print, json_dump, prepare_dump_dict
 from .parseformat import Highlander, MakePathSafeAction
-from .process import daemonize, daemonizing
+from .process import daemonize, daemonizing, ThreadRunner
 from .process import signal_handler, raising_signal_handler, sig_int, ignore_sigint, SigHup, SigTerm
 from .process import popen_with_error_handling, is_terminal, prepare_subprocess_env, create_filter_process
 from .progress import ProgressIndicatorPercent, ProgressIndicatorMessage

+ 48 - 0
src/borg/helpers/process.py

@@ -6,6 +6,7 @@ import signal
 import subprocess
 import sys
 import time
+import threading
 import traceback
 
 from .. import __version__
@@ -398,3 +399,50 @@ def create_filter_process(cmd, stream, stream_close, inbound=True):
             if borg_succeeded and rc:
                 # if borg did not succeed, we know that we killed the filter process
                 raise Error("filter %s failed, rc=%d" % (cmd, rc))
+
+
+class ThreadRunner:
+    def __init__(self, sleep_interval, target, *args, **kwargs):
+        """
+        Initialize the ThreadRunner with a target function and its arguments.
+
+        :param sleep_interval: The interval (in seconds) to sleep between executions of the target function.
+        :param target: The target function to be run in the thread.
+        :param args: The positional arguments to be passed to the target function.
+        :param kwargs: The keyword arguments to be passed to the target function.
+        """
+        self._target = target
+        self._args = args
+        self._kwargs = kwargs
+        self._sleep_interval = sleep_interval
+        self._thread = None
+        self._keep_running = threading.Event()
+        self._keep_running.set()
+
+    def _run_with_termination(self):
+        """
+        Wrapper function to check if the thread should keep running.
+        """
+        while self._keep_running.is_set():
+            self._target(*self._args, **self._kwargs)
+            # sleep up to self._sleep_interval, but end the sleep early if we shall not keep running:
+            count = 1000
+            micro_sleep = float(self._sleep_interval) / count
+            while self._keep_running.is_set() and count > 0:
+                time.sleep(micro_sleep)
+                count -= 1
+
+    def start(self):
+        """
+        Start the thread.
+        """
+        self._thread = threading.Thread(target=self._run_with_termination)
+        self._thread.start()
+
+    def terminate(self):
+        """
+        Signal the thread to stop and wait for it to finish.
+        """
+        if self._thread is not None:
+            self._keep_running.clear()
+            self._thread.join()

+ 2 - 0
src/borg/repository.py

@@ -203,6 +203,8 @@ class Repository:
 
     def info(self):
         """return some infos about the repo (must be opened first)"""
+        # note: don't do anything expensive here or separate the lock refresh into a separate method.
+        self._lock_refresh()  # do not remove, see do_with_lock()
         info = dict(
             id=self.id,
             version=self.version,