fix daemonizing: don't lose an early notify signal from the background process - #10123
Merged
ThomasWaldmann merged 1 commit intoAug 16, 2026
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10123 +/- ##
==========================================
- Coverage 87.05% 87.04% -0.01%
==========================================
Files 101 101
Lines 17848 17853 +5
Branches 2705 2709 +4
==========================================
+ Hits 15537 15541 +4
+ Misses 1609 1608 -1
- Partials 702 704 +2 ☔ View full report in Codecov by Harness. |
…d process The foreground process installs SIGTERM/SIGHUP/SIGINT handlers (via archiver.run) before it reaches the point where it actually waits for the background (grandchild) process to notify it (via os.kill). If the background process started up and signalled fast enough, the signal was delivered to the foreground while it was still between os.fork() and the waiting code, so the globally installed handler raised at an unexpected, uncaught place. The signal then escaped daemonizing(), bubbled up through repository teardown (NotLocked) and made "borg mount" exit with rc 74. This was observed flaky in CI with coverage's sys.monitoring backend on Python 3.14 (its first-branch lazy source parse widens the window) and the pyfuse3 backend (faster grandchild startup). Fix the race by blocking the notify signals before the fork in _daemonize() and waiting for them atomically in the foreground. An early signal then stays pending and is reliably picked up by the wait. The background process restores the original signal mask so it keeps normal signal handling. Use signal.sigwait() plus a SIGALRM timer (signal.setitimer) for the wait, rather than signal.sigtimedwait(): the latter does not exist on macOS, where it would raise AttributeError in the foreground and let it die before the background migrated the lock (breaking test_migrate_lock_alive). signal.SIGALRM in turn does not exist on Windows, where referencing it at module import time would raise AttributeError and break the import chain (and the Windows PyInstaller build), so guard it with hasattr, matching the defensive getattr pattern already used for the notify signals. Daemonizing is not supported on Windows anyway (no os.fork), so the empty SIGALRM list has no functional effect there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ThomasWaldmann
force-pushed
the
fix-daemonizing-early-signal
branch
from
August 16, 2026 10:10
b3cf99e to
db2ff30
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The foreground process installs SIGTERM/SIGHUP/SIGINT handlers (via
archiver.run) before it reaches the point where it actually waits for the background (grandchild) process to notify it viaos.kill(). If the background process starts up and signals fast enough, the signal is delivered to the foreground while it is still betweenos.fork()and the waiting code, so the globally installed handler raises at an unexpected, uncaught place. The signal then escapesdaemonizing(), bubbles up through repository teardown (NotLocked) and makesborg mountexit with rc 74.This was observed flaky in CI with coverage's
sys.monitoringbackend on Python 3.14 (its first-branch lazy source parse widens the window) and the pyfuse3 backend (faster grandchild startup).The fix blocks the notify signals before the fork in
_daemonize()and waits for them atomically in the foreground. An early signal then stays pending and is reliably picked up by the wait. The background process restores the original signal mask so it keeps normal signal handling.The wait uses
signal.sigwait()plus a SIGALRM timer (signal.setitimer) rather thansignal.sigtimedwait(): the latter does not exist on macOS, where it would raiseAttributeErrorin the foreground and let it die before the background migrated the lock (breakingtest_migrate_lock_alive).signal.SIGALRMin turn does not exist on Windows, where referencing it at module import time would break the import chain and the Windows PyInstaller build, so it is guarded withhasattr. Daemonizing is not supported on Windows anyway (noos.fork).Split out of #9602, where this surfaced - the bug is independent of that PR and fixes a race that is present in master today.