Skip to content

Post-commit hook rebuild timeout is a no-op on Windows (SIGALRM guard) — #791 mitigation never applies there #2148

Description

@epocamx

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

  1. Windows, graphify hook install in a repo.
  2. Commit repeatedly (an auto-commit daemon reproduces it fastest).
  3. Get-Process python* and compare process start times against ~/.cache/graphify-rebuild.log.
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions