Repository navigation
Post-commit hook rebuild timeout is a no-op on Windows (SIGALRM guard) — #791 mitigation never applies there #2148
Description
Summary
The GRAPHIFY_REBUILD_TIMEOUT watchdog added to the post-commit hook is dead code on Windows, so the timeout mitigation from #791 / #1037 never applies there. A rebuild that genuinely hangs is never reaped and survives indefinitely.
In graphify/hooks.py (branch v8, lines 121-124, and again around line 159):
_timeout = int(os.environ.get('GRAPHIFY_REBUILD_TIMEOUT', '600'))
if _timeout > 0 and hasattr(signal, 'SIGALRM'):
signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(TimeoutError(...)))
signal.alarm(_timeout)signal.SIGALRM does not exist on Windows, so the guard is always False and the alarm is never armed:
> python -c "import signal; print(hasattr(signal, 'SIGALRM'))"
False
The hook is otherwise careful on Windows (it explicitly handles DETACHED_PROCESS, CREATE_BREAKAWAY_FROM_JOB, and forces GRAPHIFY_MAX_WORKERS=1 under MSYS), which makes the silent gap easy to miss: everything else was ported, the watchdog was not.
Impact
Low but unbounded in time. Rebuilds that complete normally exit fine; the rare one that hangs stays forever, accumulating across the day on machines that commit frequently.
Observed on a hub repo whose auto-sync daemon commits roughly every 20 minutes:
~/.cache/graphify-rebuild.log: 916rebuilding graph...starts, 909 clean finishes (638Rebuilt:+ 271No code-graph topology changes detected), 0 failures.- 0 timeout lines in the entire log — not because none was needed, but because the alarm cannot fire on this platform.
- 7 starts with no matching finish. At one snapshot, 16 detached rebuild processes were still alive after 10+ hours, each at ~0.1 MB working set and ~2s CPU, long after the log recorded their rebuild as finished. They had to be killed manually.
Per-process cost is small (<1 MB), so this is not the memory exhaustion of #791. It is the tail case that #791's timeout was meant to catch, still uncovered on Windows.
Reproduction
- Windows,
graphify hook installin a repo. - Commit repeatedly (an auto-commit daemon reproduces it fastest).
Get-Process python*and compare process start times against~/.cache/graphify-rebuild.log.- Some detached rebuilds remain alive well past their logged completion, and
GRAPHIFY_REBUILD_TIMEOUTnever terminates them regardless of the value set.
Suggested fix
Replace the SIGALRM alarm with a portable watchdog, so the timeout works on every platform. A daemon threading.Timer that calls os._exit is the smallest change that preserves current behaviour on POSIX:
_timeout = int(os.environ.get('GRAPHIFY_REBUILD_TIMEOUT', '600'))
if _timeout > 0:
if hasattr(signal, 'SIGALRM'):
signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(TimeoutError(f'graphify rebuild exceeded {_timeout}s')))
signal.alarm(_timeout)
else:
import threading
def _bail():
print(f'[graphify hook] graphify rebuild exceeded {_timeout}s', flush=True)
os._exit(1)
_t = threading.Timer(_timeout, _bail)
_t.daemon = True
_t.start()os._exit is deliberate: the point is to kill a process that is already stuck, where a clean shutdown may itself be what is blocked.
Worth applying to both occurrences in hooks.py.
Environment
- graphifyy 0.9.16 (installed via uv tool); the same code is present in
v8at the time of writing, and the latest published version is 0.9.25 - Windows 11, Python 3.12.10
- Hook run through Git for Windows (MSYS)
Activity
chore: bump to 0.9.26; changelog for #2137/#2126/#2148/#2141/#2079/#2163
safishamsi commentedon Jul 24, 2026
Fixed in v0.9.26 (#2161, thanks @Rishet11). A
threading.Timerfallback now arms the rebuild timeout wheresignal.SIGALRMis unavailable (Windows), soGRAPHIFY_REBUILD_TIMEOUTis honored and a hung rebuild is terminated; the Unix path is unchanged. https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.26Squashed commit of the following:
chore: bump to 0.9.26; changelog for Graphify-Labs#2137/Graphify-Labs…