mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
fix(brain-sync): one drain lock for every writer to the artifacts repo
gstack-artifacts-init, gstack-brain-restore, gstack-gbrain-source-wireup and the skill-start merge ran git in $GSTACK_HOME without the drain's lock, so the drain could never tell an abandoned .git/index.lock from a live one. They now take the same mkdir lock (bin/gstack-brain-lock.sh): the drain and the skill-start merge skip when it is held, user-run writers wait for it (GSTACK_BRAIN_LOCK_WAIT, default 120 s), and a child of the holder (the drain's daily worktree advance, restore's wireup) inherits it by token. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
7fab2a7139
commit
988fe8fe76
5 files changed
+133
-3
No files matched your search
@@ -260,6 +260,17 @@ EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- drain lock ----
|
||||
# Every gstack writer to the artifacts repo's git state takes the same lock,
|
||||
# so a drain never meets this init's git index mid-write (and can treat an
|
||||
# old index lock as abandoned).
|
||||
. "$SCRIPT_DIR/gstack-brain-lock.sh"
|
||||
if ! gstack_brain_lock_acquire "$GSTACK_HOME" wait; then
|
||||
echo "gstack-artifacts-init: another gstack sync (pid ${_GSTACK_BRAIN_LOCK_HOLDER:-unknown}) is using $GSTACK_HOME; re-run when it finishes" >&2
|
||||
exit 1
|
||||
fi
|
||||
trap 'gstack_brain_lock_release' EXIT
|
||||
|
||||
# ---- git init ----
|
||||
if [ ! -d "$GSTACK_HOME/.git" ]; then
|
||||
git -C "$GSTACK_HOME" init -q -b main 2>/dev/null || git -C "$GSTACK_HOME" init -q
|
||||
@@ -282,7 +293,7 @@ EOF
|
||||
ALLOWLIST="$GSTACK_HOME/.brain-allowlist"
|
||||
ALLOWLIST_TMP=$(mktemp "$GSTACK_HOME/.brain-allowlist.XXXXXX")
|
||||
ALLOWLIST_ASSEMBLED=$(mktemp "$GSTACK_HOME/.brain-allowlist.XXXXXX")
|
||||
trap 'rm -f "$ALLOWLIST_TMP" "$ALLOWLIST_ASSEMBLED"' EXIT
|
||||
trap 'rm -f "$ALLOWLIST_TMP" "$ALLOWLIST_ASSEMBLED"; gstack_brain_lock_release' EXIT
|
||||
ALLOWLIST_MARKER='# ---- USER ADDITIONS BELOW ---- (survives re-init; above is managed)'
|
||||
|
||||
cat > "$ALLOWLIST_TMP" <<'EOF'
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# gstack-brain-lock.sh — the one drain lock for every gstack writer to the
|
||||
# artifacts repo's git state in $GSTACK_HOME.
|
||||
#
|
||||
# This file is NOT executable; source it:
|
||||
#
|
||||
# . "$(dirname "$0")/gstack-brain-lock.sh"
|
||||
#
|
||||
# Writers that take it: the drain (gstack-brain-sync --once), the skill-start
|
||||
# merge (gstack-skill-start), gstack-artifacts-init, gstack-brain-restore and
|
||||
# gstack-gbrain-source-wireup. Holding it is what lets the drain treat an old
|
||||
# .git/index.lock as abandoned: no other gstack writer can be running git there.
|
||||
#
|
||||
# Provides:
|
||||
# gstack_brain_lock_acquire <gstack_home> <try|wait>
|
||||
# Returns 0 when this process holds the lock afterwards, 1 otherwise.
|
||||
# try — give up at once when a live process holds it.
|
||||
# wait — retry once a second for GSTACK_BRAIN_LOCK_WAIT seconds (default
|
||||
# 120), then give up.
|
||||
# A child of the holder (the drain runs the wireup's daily advance; restore
|
||||
# runs the wireup) inherits GSTACK_BRAIN_LOCK_TOKEN; when it matches the
|
||||
# lock's owner file the child proceeds without taking or releasing it.
|
||||
# On failure, _GSTACK_BRAIN_LOCK_HOLDER names the holder's pid (or "").
|
||||
# gstack_brain_lock_release
|
||||
# Removes the lock only when this process took it and still owns it.
|
||||
#
|
||||
# The lock is a directory (mkdir is atomic on every POSIX filesystem; flock(1)
|
||||
# is not on macOS) holding `pid` and `owner`. A lock whose pid is dead is
|
||||
# stale and is cleared. A lock directory with no pid file is a holder killed
|
||||
# between mkdir and the pid write once it is older than 10 minutes.
|
||||
|
||||
_GSTACK_BRAIN_LOCK_DIR=""
|
||||
_GSTACK_BRAIN_LOCK_MINE=0
|
||||
_GSTACK_BRAIN_LOCK_HOLDER=""
|
||||
|
||||
_gstack_brain_lock_try_once() {
|
||||
local dir="$1" pid
|
||||
if mkdir "$dir" 2>/dev/null; then
|
||||
GSTACK_BRAIN_LOCK_TOKEN="$$-${RANDOM:-0}-$(date +%s 2>/dev/null || echo 0)"
|
||||
export GSTACK_BRAIN_LOCK_TOKEN
|
||||
printf '%s\n' "$$" > "$dir/pid" 2>/dev/null || true
|
||||
printf '%s\n' "$GSTACK_BRAIN_LOCK_TOKEN" > "$dir/owner" 2>/dev/null || true
|
||||
_GSTACK_BRAIN_LOCK_MINE=1
|
||||
return 0
|
||||
fi
|
||||
if [ -f "$dir/pid" ]; then
|
||||
pid=$(head -1 "$dir/pid" 2>/dev/null || echo "")
|
||||
_GSTACK_BRAIN_LOCK_HOLDER="$pid"
|
||||
case "$pid" in ''|*[!0-9]*) return 1 ;; esac
|
||||
kill -0 "$pid" 2>/dev/null && return 1
|
||||
elif [ -z "$(find "$dir" -maxdepth 0 -mmin +10 2>/dev/null)" ]; then
|
||||
_GSTACK_BRAIN_LOCK_HOLDER=""
|
||||
return 1
|
||||
fi
|
||||
rm -rf "$dir" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
gstack_brain_lock_acquire() {
|
||||
local home="$1" mode="${2:-try}" waited=0 limit
|
||||
_GSTACK_BRAIN_LOCK_DIR="$home/.brain-sync.lock.d"
|
||||
_GSTACK_BRAIN_LOCK_MINE=0
|
||||
_GSTACK_BRAIN_LOCK_HOLDER=""
|
||||
if [ -n "${GSTACK_BRAIN_LOCK_TOKEN:-}" ] && [ -f "$_GSTACK_BRAIN_LOCK_DIR/owner" ] \
|
||||
&& [ "$(head -1 "$_GSTACK_BRAIN_LOCK_DIR/owner" 2>/dev/null)" = "$GSTACK_BRAIN_LOCK_TOKEN" ]; then
|
||||
return 0
|
||||
fi
|
||||
mkdir -p "$home" 2>/dev/null || true
|
||||
limit="${GSTACK_BRAIN_LOCK_WAIT:-120}"
|
||||
case "$limit" in ''|*[!0-9]*) limit=120 ;; esac
|
||||
while :; do
|
||||
_gstack_brain_lock_try_once "$_GSTACK_BRAIN_LOCK_DIR" && return 0
|
||||
# A stale lock was just cleared: take it now instead of after a sleep.
|
||||
[ -d "$_GSTACK_BRAIN_LOCK_DIR" ] || { _gstack_brain_lock_try_once "$_GSTACK_BRAIN_LOCK_DIR" && return 0; }
|
||||
[ "$mode" = "wait" ] || return 1
|
||||
[ "$waited" -ge "$limit" ] && return 1
|
||||
sleep 1
|
||||
waited=$(( waited + 1 ))
|
||||
done
|
||||
}
|
||||
|
||||
gstack_brain_lock_release() {
|
||||
[ "$_GSTACK_BRAIN_LOCK_MINE" = "1" ] || return 0
|
||||
if [ "$(head -1 "$_GSTACK_BRAIN_LOCK_DIR/owner" 2>/dev/null)" = "${GSTACK_BRAIN_LOCK_TOKEN:-}" ]; then
|
||||
rm -rf "$_GSTACK_BRAIN_LOCK_DIR" 2>/dev/null || true
|
||||
fi
|
||||
_GSTACK_BRAIN_LOCK_MINE=0
|
||||
unset GSTACK_BRAIN_LOCK_TOKEN
|
||||
return 0
|
||||
}
|
||||
@@ -89,9 +89,18 @@ EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- drain lock ----
|
||||
# Every gstack writer to the artifacts repo's git state takes the same lock;
|
||||
# the wireup this script runs at the end inherits it.
|
||||
. "$SCRIPT_DIR/gstack-brain-lock.sh"
|
||||
if ! gstack_brain_lock_acquire "$GSTACK_HOME" wait; then
|
||||
echo "gstack-brain-restore: another gstack sync (pid ${_GSTACK_BRAIN_LOCK_HOLDER:-unknown}) is using $GSTACK_HOME; re-run when it finishes" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---- clone to staging ----
|
||||
STAGING=$(mktemp -d "${TMPDIR:-/tmp}/gstack-brain-restore.XXXXXX")
|
||||
trap 'rm -rf "$STAGING" 2>/dev/null' EXIT
|
||||
trap 'rm -rf "$STAGING" 2>/dev/null; gstack_brain_lock_release' EXIT
|
||||
|
||||
echo "Cloning $REMOTE_URL to staging..."
|
||||
RESTORE_HOST="${REMOTE_URL#*://}"; RESTORE_HOST="${RESTORE_HOST#*@}"; RESTORE_HOST="${RESTORE_HOST%%[/:]*}"
|
||||
|
||||
@@ -56,6 +56,17 @@ fi
|
||||
PLIST_PATH="$HOME/Library/LaunchAgents/com.gstack.brain-sync.plist"
|
||||
GBRAIN_CONFIG="$HOME/.gbrain/config.json"
|
||||
|
||||
# Git writes to $GSTACK_HOME (worktree add/prune/remove, the daily advance)
|
||||
# take the drain lock every gstack writer shares. A caller that already holds
|
||||
# it (the drain's daily advance, gstack-brain-restore) passes it down.
|
||||
. "$SCRIPT_DIR/gstack-brain-lock.sh"
|
||||
trap 'gstack_brain_lock_release' EXIT
|
||||
take_drain_lock() {
|
||||
[ -d "$GSTACK_HOME/.git" ] || return 0
|
||||
gstack_brain_lock_acquire "$GSTACK_HOME" wait \
|
||||
|| die "another gstack sync (pid ${_GSTACK_BRAIN_LOCK_HOLDER:-unknown}) is using $GSTACK_HOME; re-run when it finishes" 1
|
||||
}
|
||||
|
||||
# ---- arg parse ----
|
||||
MODE="wireup"
|
||||
STRICT=0
|
||||
@@ -285,10 +296,12 @@ do_wireup() {
|
||||
# Capture ensure_worktree's return code explicitly. `$?` after `||` reflects
|
||||
# the LAST command in the function under set -e, which is unreliable when the
|
||||
# function has multiple internal exit paths.
|
||||
take_drain_lock
|
||||
set +e
|
||||
ensure_worktree
|
||||
ew_rc=$?
|
||||
set -e
|
||||
gstack_brain_lock_release
|
||||
case "$ew_rc" in
|
||||
0) : ;; # success
|
||||
2)
|
||||
@@ -403,6 +416,7 @@ do_advance_only() {
|
||||
exit 0
|
||||
fi
|
||||
local sha cur
|
||||
take_drain_lock
|
||||
sha=$(git -C "$GSTACK_HOME" rev-parse HEAD 2>/dev/null) || { warn "advance-only: cannot read parent HEAD"; exit 0; }
|
||||
cur=$(git -C "$WORKTREE" rev-parse HEAD 2>/dev/null || echo "")
|
||||
if [ "$cur" = "$sha" ]; then
|
||||
@@ -426,6 +440,7 @@ do_uninstall() {
|
||||
fi
|
||||
|
||||
if [ -d "$WORKTREE/.git" ] || [ -f "$WORKTREE/.git" ]; then
|
||||
take_drain_lock
|
||||
git -C "$GSTACK_HOME" worktree remove --force "$WORKTREE" 2>/dev/null \
|
||||
|| safe_rm_worktree "$WORKTREE"
|
||||
fi
|
||||
|
||||
@@ -320,12 +320,18 @@ if [ -d "$_GH/.git" ] && [ "$_BRAIN_SYNC_MODE" != "off" ]; then
|
||||
# fail-closed (same wiring as bin/gstack-brain-sync's fetch/push).
|
||||
. "$_BIN/gstack-egress-lib.sh" 2>/dev/null || true
|
||||
_PULL_HOST=$(cd "$_GH" 2>/dev/null && git remote get-url origin 2>/dev/null | sed -E 's|^[a-z+]+://([^/@]*@)?([^/:]+).*|\2|; s|^([^@]+@)?([^:]+):.*|\2|' | head -1)
|
||||
if command -v _receipted_git >/dev/null 2>&1; then
|
||||
# The merge writes the artifacts repo's git state, so it takes the drain
|
||||
# lock every gstack writer shares; when a drain holds it, the pull waits
|
||||
# for the next start (no stamp).
|
||||
. "$_BIN/gstack-brain-lock.sh" 2>/dev/null || true
|
||||
if command -v _receipted_git >/dev/null 2>&1 && command -v gstack_brain_lock_acquire >/dev/null 2>&1 \
|
||||
&& gstack_brain_lock_acquire "$_GH" try; then
|
||||
# Non-interactive + slow-network bounded (same guards as gstack-update-check):
|
||||
# a hung remote or a credential prompt must not stall the preamble.
|
||||
( cd "$_GH" && GSTACK_HOME="$_GH" GIT_TERMINAL_PROMPT=0 GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 \
|
||||
_receipted_git closed brain-sync "${_PULL_HOST:-unknown}" curated-memory-git-fetch "artifacts_sync_mode!=off" \
|
||||
git fetch origin >/dev/null 2>&1 && git merge --ff-only "origin/$(git rev-parse --abbrev-ref HEAD)" >/dev/null 2>&1 ) || true
|
||||
gstack_brain_lock_release
|
||||
# Stamp only when the receipted path actually ran — a missing egress lib
|
||||
# must surface as a retry next start, not a silent 24h suppression.
|
||||
echo "$_BRAIN_NOW" > "$_BRAIN_LAST_PULL_FILE"
|
||||
|
||||
Reference in new issue
Block a user