fix(3597)(3347): write graphify rebuild lock in parent hook to close ENOTEMPTY race

The hook double-forked the rebuild subprocess and returned before the
subprocess wrote .planning/graphs/.rebuild.lock. Callers (notably the
feat-3347 test cleanup) waited for the lock to disappear before
rm -rf'ing the tmpdir, but an absent lock was ambiguous: it could mean
"subprocess finished and trapped lock removal" OR "subprocess hasn't
started yet." Under ubuntu CI load the second case won, cleanup raced
ahead, and rmSync walked into .planning/graphs while the subprocess
was still creating files — surfacing as
  ENOTEMPTY: directory not empty, rmdir '/tmp/gsd-3347-*/.planning/graphs'
on the "dispatches on: git commit -m fix" test.

Spawn the rebuild as a regular backgrounded job, capture $!, and write
the lock file synchronously in the parent before exit. Lock-presence
is now a reliable in-flight signal; the rebuild script's existing
trap-on-EXIT rm still owns cleanup.

Validated: holodeck (ubuntu docker) 11224 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-16 11:39:08 -04:00
parent 7fa5eb7e63
commit f8eda5bf16

View File

@@ -134,19 +134,21 @@ HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
REBUILD_SCRIPT="$HOOK_DIR/lib/gsd-graphify-rebuild.sh"
[ -f "$REBUILD_SCRIPT" ] || exit 0
# Detach the rebuild. Portable double-fork via subshell + disown — works on
# macOS (no setsid) and Linux. Redirect all I/O to /dev/null so the hook
# returns instantly even if the child keeps stdout/stderr handles open.
(
bash "$REBUILD_SCRIPT" \
"$STATUS_FILE" \
"$LOCK_FILE" \
"$HEAD_SHA" \
"$MS_START" \
"$GRAPHIFY_BIN" \
</dev/null >/dev/null 2>&1 &
disown
) &
disown
# Detach the rebuild. Spawn as a regular background job so we can capture
# its PID via $! and write it to the lock file synchronously here in the
# parent. This eliminates a startup race where a caller (e.g. test cleanup)
# observing an absent lock could not distinguish "subprocess finished" from
# "subprocess hasn't started yet." With the lock written before this hook
# returns, lock-presence is a reliable in-flight signal.
bash "$REBUILD_SCRIPT" \
"$STATUS_FILE" \
"$LOCK_FILE" \
"$HEAD_SHA" \
"$MS_START" \
"$GRAPHIFY_BIN" \
</dev/null >/dev/null 2>&1 &
REBUILD_PID=$!
echo "$REBUILD_PID" > "$LOCK_FILE"
disown "$REBUILD_PID" 2>/dev/null || true
exit 0