Skip to content

Commit 3705414

Browse files
committed
fix(lock): release flock when Ctrl+C orphans docker children
The flock run-lock is held against the open file description, and bash sets no close-on-exec on the {_LOCK_FD} redirection fd. Every docker/ compose child and every &-spawned parallel worker therefore inherits the fd and keeps the lock held until its own copy closes. A Ctrl+C during `docker compose pull` could orphan a docker subprocess (the signal handler's `pkill -P` reaches only direct children, racily), and that orphan kept the inherited fd — and thus the lock — alive, wedging every later run with "Another hoist run holds the lock". After acquiring the lock, hand the held fd to one childless holder process and close our own copy so nothing we fork or exec afterwards can pin the lock. `disown` keeps the holder out of the job table so the parallel pool's `wait`/`wait -n` ignore it; the EXIT trap (also reached via _on_signal's `exit` on INT/TERM) kills the holder, releasing the lock the instant hoist truly exits — regardless of orphaned children. Bump to v1.8.2.
1 parent d77ae80 commit 3705414

2 files changed

Lines changed: 18 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Each `process_container` invocation appends outcome tokens (`updated`, `update_f
7979

8080
### Run lock and exit code
8181

82-
`_acquire_run_lock` (`hoist.sh:406`) takes an exclusive `flock` on `${CACHE_LOCATION}/hoist.lock` at startup (with an `mkdir`-plus-stale-PID fallback when `flock` is absent). A second run that finds the lock held logs and exits 0 rather than racing over shared state; `--list`/`--status` skips the lock. After `print_summary`, hoist **exits non-zero** if any container produced `update_failed`, `unhealthy`, or `rollback_failed` (drives `Type=oneshot` systemd, cron `MAILTO`, CI). Boolean gates (`update`/`notify`/`rollback`/`healthcheck.wait`, `ROLLBACK_DEFAULT`) are routed through `_is_true`, which accepts `true|1|yes|on` case-insensitively.
82+
`_acquire_run_lock` (`hoist.sh:417`) takes an exclusive `flock` on `${CACHE_LOCATION}/hoist.lock` at startup (with an `mkdir`-plus-stale-PID fallback when `flock` is absent). A second run that finds the lock held logs and exits 0 rather than racing over shared state; `--list`/`--status` skips the lock. **flock fd handoff:** bash sets no close-on-exec on the `{_LOCK_FD}` redirection fd, so docker/compose children and `&`-spawned workers would otherwise inherit it and keep the lock held — a Ctrl+C that orphans a `docker compose pull` subprocess would wedge every later run with "Another hoist run holds the lock". So after acquiring, hoist hands the held fd to a single childless holder process (`( exec sleep … ) &`, `disown`ed so the parallel pool's `wait`/`wait -n` ignore it), closes its own copy of the fd, and arms an EXIT trap (also reached via `_on_signal`'s `exit`) that kills the holder — releasing the lock the instant hoist truly exits, regardless of orphaned docker children. After `print_summary`, hoist **exits non-zero** if any container produced `update_failed`, `unhealthy`, or `rollback_failed` (drives `Type=oneshot` systemd, cron `MAILTO`, CI). Boolean gates (`update`/`notify`/`rollback`/`healthcheck.wait`, `ROLLBACK_DEFAULT`) are routed through `_is_true`, which accepts `true|1|yes|on` case-insensitively.
8383

8484
### Self-update
8585

hoist.sh

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
2-
HOIST_VERSION="1.8.1"
2+
HOIST_VERSION="1.8.2"
33
HOIST_REPO="KingPin/hoist"
44

55
DOCKER_BINARY="${DOCKER_BINARY:-$(which docker)}"
@@ -427,6 +427,22 @@ _acquire_run_lock() {
427427
log "Another hoist run holds the lock ($lock); exiting."
428428
exit 0
429429
fi
430+
# flock is held against the open file description, not this process, and
431+
# bash does NOT set close-on-exec on {var}> fds — so every docker/compose
432+
# child we exec and every &-spawned worker inherits _LOCK_FD and keeps the
433+
# lock held until its own copy closes. A Ctrl+C mid-`docker compose pull`
434+
# can orphan a docker subprocess that keeps the inherited fd open, wedging
435+
# every later run with "Another hoist run holds the lock". Hand the held fd
436+
# to one childless holder process, then close our copy so nothing we exec
437+
# or fork afterwards can pin the lock. `disown` keeps the holder out of the
438+
# job table (so the parallel pool's `wait`/`wait -n` ignore it); the EXIT
439+
# trap — which also runs after _on_signal's `exit` on INT/TERM — kills the
440+
# holder, releasing the lock the instant hoist truly exits.
441+
( exec sleep 2147483647 ) &
442+
_LOCK_HOLDER=$!
443+
disown
444+
exec {_LOCK_FD}>&-
445+
trap '[[ -n "${_LOCK_HOLDER:-}" ]] && kill "$_LOCK_HOLDER" 2>/dev/null' EXIT
430446
return 0
431447
fi
432448
# flock unavailable: atomic mkdir lock. mkdir fails if the dir exists, which

0 commit comments

Comments
 (0)