mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 15:09:00 +02:00
fix(session-update): lock pidfile records the live holder; hard TTL bounds every wedge (#2613)
echo $$ inside the backgrounded subshell recorded the PARENT hook's PID —
which exits immediately — so every subsequent session judged the lock stale
and rm -rf'd a LIVE holder's lock, letting concurrent updaters run over each
other. The pidfile now records ${BASHPID:-$(sh -c 'echo $PPID')} (macOS
bash 3.2 has no BASHPID; the sh child's PPID is exactly this subshell).
Staleness is now two independent detectors: PID liveness (as before, but
against the real holder), and a 30-minute hard TTL on the heartbeat mtime —
reclaimed regardless of kill -0, so a recycled PID or hung holder can't wedge
the lock forever. The holder touches the pidfile after the pull and after
setup, so a legitimately-slow run keeps itself alive. Empty and missing
pidfiles are respected inside the TTL window (the mkdir→echo race) and
reclaimed past it.
Fixes #2613.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
63ef693d02
commit
ddeeb18edb
@@ -54,26 +54,53 @@ fi
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# ── Acquire lockfile (skip if another session is running setup) ──
|
||||
#
|
||||
# Staleness has two independent detectors (#2613):
|
||||
# 1. PID liveness — the pidfile records the HOLDER subshell's PID and a
|
||||
# dead PID means reclaim. ($BASHPID, never $$: $$ expands to the PARENT
|
||||
# hook's PID even inside this backgrounded subshell, and the parent
|
||||
# exits immediately — so every later session judged the lock stale and
|
||||
# rm -rf'd a LIVE holder's lock, letting concurrent updaters in.)
|
||||
# 2. Hard TTL on the heartbeat mtime — reclaim regardless of kill -0, so a
|
||||
# recycled PID or a hung holder can't wedge the lock forever. The
|
||||
# holder touches the pidfile at step boundaries (after the pull, after
|
||||
# setup), so a legitimately-slow run keeps itself alive. The TTL also
|
||||
# bounds the missing/empty-pidfile states: inside the window they mean
|
||||
# "just acquired, between mkdir and echo" and are respected.
|
||||
LOCK_TTL_MINUTES=30
|
||||
lock_is_expired() {
|
||||
_hb="$LOCK_DIR/pid"
|
||||
[ -f "$_hb" ] || _hb="$LOCK_DIR"
|
||||
[ -n "$(find "$_hb" -maxdepth 0 -mmin +$LOCK_TTL_MINUTES 2>/dev/null)" ]
|
||||
}
|
||||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||||
# Lock exists — check if stale (PID dead)
|
||||
if [ -f "$LOCK_DIR/pid" ]; then
|
||||
if lock_is_expired; then
|
||||
rm -rf "$LOCK_DIR" 2>/dev/null
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || { log_entry "SKIP lock_contested"; exit 0; }
|
||||
log_entry "RECLAIMED lock_ttl_expired"
|
||||
elif [ -f "$LOCK_DIR/pid" ]; then
|
||||
LOCK_PID=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo 0)
|
||||
if [ "$LOCK_PID" -gt 0 ] 2>/dev/null && ! kill -0 "$LOCK_PID" 2>/dev/null; then
|
||||
# Stale lock — remove and re-acquire
|
||||
rm -rf "$LOCK_DIR" 2>/dev/null
|
||||
mkdir "$LOCK_DIR" 2>/dev/null || { log_entry "SKIP lock_contested"; exit 0; }
|
||||
else
|
||||
# Live holder — or an empty/non-numeric pidfile inside the TTL
|
||||
# window (the -gt test fails on garbage, landing here by design).
|
||||
log_entry "SKIP locked_by=$LOCK_PID"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
# Missing pidfile inside the TTL window: just-acquired (mkdir→echo race).
|
||||
log_entry "SKIP locked_no_pid"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write PID for stale lock detection
|
||||
echo $$ > "$LOCK_DIR/pid" 2>/dev/null
|
||||
# Write the HOLDER's PID for stale lock detection (see #2613 note above;
|
||||
# macOS ships bash 3.2 with no BASHPID — the sh child's $PPID IS this
|
||||
# subshell, so the fallback is exact there).
|
||||
echo "${BASHPID:-$(sh -c 'echo $PPID')}" > "$LOCK_DIR/pid" 2>/dev/null
|
||||
|
||||
# Clean up lock on exit
|
||||
trap 'rm -rf "$LOCK_DIR" 2>/dev/null' EXIT
|
||||
@@ -94,6 +121,9 @@ fi
|
||||
PULL_EXIT=$?
|
||||
NEW_HEAD=$(git -C "$GSTACK_DIR" rev-parse HEAD 2>/dev/null)
|
||||
|
||||
# Heartbeat: pull done — keep the TTL clock fresh for the setup step.
|
||||
touch "$LOCK_DIR/pid" 2>/dev/null
|
||||
|
||||
# Record check time regardless of outcome
|
||||
date +%s > "$THROTTLE_FILE" 2>/dev/null
|
||||
|
||||
@@ -132,6 +162,8 @@ fi
|
||||
( cd "$GSTACK_DIR" && ./setup -q ) >/dev/null 2>&1 || {
|
||||
log_entry "SETUP_FAILED"
|
||||
}
|
||||
# Heartbeat: setup done (either way) — refresh the TTL clock.
|
||||
touch "$LOCK_DIR/pid" 2>/dev/null
|
||||
else
|
||||
log_entry "SETUP_SKIPPED bun_missing"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user