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: 916 rebuilding graph... starts, 909 clean finishes (638 Rebuilt: + 271 No 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 install in 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_TIMEOUT never 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
v8 at 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)
Summary
The
GRAPHIFY_REBUILD_TIMEOUTwatchdog 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(branchv8, lines 121-124, and again around line 159):signal.SIGALRMdoes not exist on Windows, so the guard is alwaysFalseand the alarm is never armed:The hook is otherwise careful on Windows (it explicitly handles
DETACHED_PROCESS,CREATE_BREAKAWAY_FROM_JOB, and forcesGRAPHIFY_MAX_WORKERS=1under 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.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
graphify hook installin a repo.Get-Process python*and compare process start times against~/.cache/graphify-rebuild.log.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.Timerthat callsos._exitis the smallest change that preserves current behaviour on POSIX:os._exitis 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
v8at the time of writing, and the latest published version is 0.9.25