mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-08 17:58:13 +01:00
fix(lease): second /ship review pass
- orphaned() reads the clock before the lock's mtime and requires the mtime unchanged at the end, so a lock made meanwhile is never removed; an mtime a minute or more ahead (clock skew) counts as old. - Claims older than 2 minutes are abandoned and pruned by touch. - A waiter skips the claim and mkdir while the lock is visibly held; the wait is bounded at 300 tries or 30 s; a non-numeric tries value falls back to 300; ten quick retries (not three) before a lock nobody holds is reported as untakeable; pid 1 is a valid holder again. - Tests: deterministic young-lock clock, skew and non-numeric owner shapes, abandoned claims, the deadline, explicit 30 s timeouts. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a94c167598
commit
3f6ebca2a5
4 files changed
+98
-27
No files matched your search
+30
-15
@@ -109,19 +109,23 @@ state() {
|
||||
# taken by a live claimant or orphaned by a dead one. A lock is broken only
|
||||
# when its holder is dead or its pid now belongs to another process, never by
|
||||
# age, and unlock removes only the caller's own lock. An ownerless lock is
|
||||
# broken once no claimant lives and it is at least 2 s old, so a taker between
|
||||
# its mkdir and its owner rename is never taken for a crash, even when its
|
||||
# claim landed after the claims were read. A symlink .lock left by an older
|
||||
# broken once no claimant lives and it is at least 2 s old (and unchanged
|
||||
# while it was checked), so a taker between its mkdir and its owner rename is
|
||||
# never taken for a crash, even when its claim landed after the claims were
|
||||
# read; a claim older than 2 minutes is abandoned, since a live taker holds
|
||||
# one for milliseconds. An owner token whose pid is 0 or not a number counts
|
||||
# as a dead holder. A symlink .lock left by an older
|
||||
# gstack names its holder the same way and excludes a directory lock at the
|
||||
# same path. Known limit: the reverse does not hold, since an older gstack's
|
||||
# ln -s onto a directory lock links inside it and succeeds; only a process
|
||||
# that started before an upgrade of this checkout can run that code. A live holder
|
||||
# makes lock() give up after GSTACK_LEASE_LOCK_TRIES tries 0.1 s apart
|
||||
# (default 300).
|
||||
# (default 300) or 30 s, whichever comes first.
|
||||
LOCK="$DIR/.lock" TOKEN=""
|
||||
# take PATH — create the lock PATH owned by TOKEN; 1 when it exists.
|
||||
take() {
|
||||
printf '%s\n' "$TOKEN" > "$1.claim.$$" || { rm -f "$1.claim.$$"; return 1; }
|
||||
if [ -e "$1" ] || [ -L "$1" ]; then return 1; fi
|
||||
{ printf '%s\n' "$TOKEN" > "$1.claim.$$"; } 2>/dev/null || { rm -f "$1.claim.$$"; return 1; }
|
||||
if mkdir "$1" 2>/dev/null; then
|
||||
mv -f "$1.claim.$$" "$1/owner" && return 0
|
||||
rm -rf "$1"
|
||||
@@ -132,19 +136,27 @@ take() {
|
||||
# owner_of PATH — the token the lock PATH names, empty when it names none.
|
||||
owner_of() { if [ -L "$1" ]; then readlink "$1" 2>/dev/null; else head -1 "$1/owner" 2>/dev/null; fi; }
|
||||
# orphaned PATH — PATH is a lock (not a legacy symlink) that names no owner,
|
||||
# has no live claimant and is at least 2 s old. Claims are read before the
|
||||
# owner, so a taker that renames its claim in between is seen as the owner;
|
||||
# the age is read last, so a lock made since is too new. An empty claim is a
|
||||
# taker that has not reached its mkdir, so it never made this lock.
|
||||
# has no live claimant younger than 2 minutes, is at least 2 s old and kept
|
||||
# its mtime throughout. The clock is read before the first mtime, so a pause
|
||||
# only makes the lock look younger, and a lock made since (a new generation)
|
||||
# has a newer mtime. Claims are read before the owner, so a taker that renames
|
||||
# its claim in between is seen as the owner. An empty claim is a taker that
|
||||
# has not reached its mkdir, so it never made this lock. An mtime a minute or
|
||||
# more in the future (clock skew) counts as old.
|
||||
mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }
|
||||
orphaned() {
|
||||
_oclock=$(date +%s)
|
||||
[ -e "$1" ] && [ ! -L "$1" ] || return 1
|
||||
_om=$(mtime "$1") || return 1
|
||||
for _c in "$1".claim.*; do
|
||||
[ -n "$(find "$_c" -mmin -2 2>/dev/null)" ] || continue
|
||||
_t=$(head -1 "$_c" 2>/dev/null)
|
||||
[ -n "$_t" ] && ! holder_gone "$_t" && return 1
|
||||
done
|
||||
[ -z "$(owner_of "$1")" ] || return 1
|
||||
_m=$(stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null) || return 1
|
||||
[ $(( $(date +%s) - _m )) -ge 2 ]
|
||||
[ "$(mtime "$1")" = "$_om" ] || return 1
|
||||
_oage=$((_oclock - _om))
|
||||
[ "$_oage" -ge 2 ] || [ "$_oage" -le -60 ]
|
||||
}
|
||||
# drop PATH — remove a lock in one step: a directory is renamed aside first,
|
||||
# so nobody sees it ownerless on the way out. Once renamed the lock is
|
||||
@@ -157,26 +169,28 @@ drop() {
|
||||
}
|
||||
lock() {
|
||||
[ -n "$TOKEN" ] || TOKEN="$$ $(identity $$)"
|
||||
_tries=0 _gone=0
|
||||
_tries=0 _gone=0 _max=${GSTACK_LEASE_LOCK_TRIES:-300} _start=$SECONDS
|
||||
case "$_max" in ''|*[!0-9]*) _max=300 ;; esac
|
||||
until take "$LOCK"; do
|
||||
LOCK_OWNER=$(owner_of "$LOCK")
|
||||
if [ ! -e "$LOCK" ] && [ ! -L "$LOCK" ]; then
|
||||
# Released since take, or take itself cannot work here: never wait out
|
||||
# the tries for a lock nobody holds.
|
||||
_gone=$((_gone + 1))
|
||||
[ "$_gone" -ge 3 ] && return 1
|
||||
[ "$_gone" -ge 10 ] && return 1
|
||||
continue
|
||||
fi
|
||||
_gone=0 _tries=$((_tries + 1))
|
||||
if [ -n "$LOCK_OWNER" ]; then holder_gone "$LOCK_OWNER" && break_lock "$LOCK_OWNER" && continue
|
||||
elif orphaned "$LOCK"; then break_lock "" && continue
|
||||
fi
|
||||
[ "$_tries" -ge "${GSTACK_LEASE_LOCK_TRIES:-300}" ] && return 1
|
||||
[ "$_tries" -ge "$_max" ] && return 1
|
||||
[ $((SECONDS - _start)) -ge 30 ] && return 1
|
||||
sleep 0.1
|
||||
done
|
||||
}
|
||||
holder_gone() {
|
||||
case "${1%% *}" in ''|*[!0-9]*|0|1) return 0 ;; esac
|
||||
case "${1%% *}" in ''|*[!0-9]*|0) return 0 ;; esac
|
||||
case "$(state "${1%% *}" "${1#* }" 0)" in dead|stale) return 0 ;; esac; return 1
|
||||
}
|
||||
# break_lock OWNER — remove the lock only if it still names OWNER (empty: is
|
||||
@@ -235,6 +249,7 @@ case "$VERB" in
|
||||
_tmp="$DIR/.$PID.$$"
|
||||
identity "$PID" > "$_tmp" && mv -f "$_tmp" "$DIR/$PID" || { rm -f "$_tmp"; unlock; exit 2; }
|
||||
find "$DIR" -maxdepth 1 -mmin +120 -type f ! -name '.lock*' -exec rm -f {} + 2>/dev/null
|
||||
find "$DIR" -maxdepth 1 -mmin +2 -type f -name '.lock*.claim.*' -exec rm -f {} + 2>/dev/null
|
||||
rm -rf "$DIR"/.lock*.gone.* 2>/dev/null
|
||||
unlock
|
||||
exit 0 ;;
|
||||
|
||||
Reference in new issue
Block a user