Skip to content

Commit 08fdf06

Browse files
committed
osdc/hf-cache: fix prepare-host-mount init crash-loop on shadowed /mnt/hf_cache
#876's self-heal init can crash-loop instead of healing, leaving the cache mount silently absent on a node whose startup taint is already cleared — so CI jobs land on the node and read an empty /mnt/hf_cache (observed on 20 ue2 nodes, 29 job pods, incl. a failed pytorch inductor job). Root cause, all in the prepare-host-mount init: - `mount --bind /mnt /mnt` is non-recursive, so re-binding /mnt shadows a pre-existing /mnt/hf_cache submount (e.g. one left by an earlier init version that bound /mnt/hf_cache directly on the root fs). - the guard `grep -q " /mnt/hf_cache " /proc/mounts` matches that shadowed, now-unreachable mount, so the re-bind is skipped. - `mount --make-rshared /mnt/hf_cache` then runs on a live path that is not a mountpoint -> "not mount point or bad option" -> `set -e` -> the init exits non-zero and CrashLoopBackOffs forever (80+ restarts seen). Fix: - guard on the LIVE path with `mountpoint -q`, never a /proc/mounts scan, so a shadowed orphan can't make us skip the bind. - use `mount --rbind` for /mnt so an existing submount is carried into the new bind instead of being shadowed. - tolerate `make-rshared` failures (|| true) so a transient state can't turn into a crash-loop. - peel any occupant of the live /mnt/hf_cache (dead FUSE or stale xfs bind) with a bounded loop, not one gated on `stat` (a stale bind stats fine). just lint + just test pass. Smoke test's `make-rshared` assertion still holds.
1 parent 494edf2 commit 08fdf06

1 file changed

Lines changed: 36 additions & 18 deletions

File tree

osdc/modules/hf-cache/kubernetes/mount-daemonset.yaml.tpl

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,25 +73,43 @@ spec:
7373
- -c
7474
- |
7575
set -eu
76-
exec /proc/1/root/usr/bin/nsenter -t 1 -m -- /bin/sh -c '
77-
mkdir -p /mnt/hf_cache /mnt/hf-cache-vfs
78-
# A crashed rclone leaves a dead FUSE at /mnt/hf_cache that fails to stat,
79-
# wedging every pod on the node in CreateContainerError. rclone self-heals
80-
# on in-place restart (it Bidirectional-mounts the parent /mnt, always a
81-
# live dir), but a pod recreation only runs this init — so clear the dead
82-
# endpoint here. fusermount3 is the FUSE3 binary on AL2023 hosts (fusermount
83-
# is not installed); umount -l is the last-resort peel. Both leave the bind
84-
# under /mnt intact.
85-
while ! stat /mnt/hf_cache >/dev/null 2>&1; do
86-
fusermount3 -uz /mnt/hf_cache 2>/dev/null || umount -l /mnt/hf_cache 2>/dev/null || break
76+
# Runs in the host (PID 1) mount namespace. Every guard below tests the
77+
# LIVE path with mountpoint(1) — never a raw `grep /proc/mounts` scan. A
78+
# table scan also matches a mount that has been shadowed by a later bind
79+
# of a parent dir (an orphaned /mnt/hf_cache hidden under the /mnt bind).
80+
# Acting on such a mount by path fails ("not a mountpoint"), and under
81+
# set -e that turned this init into an unrecoverable CrashLoopBackOff while
82+
# the node's startup taint was already cleared — so jobs kept landing on a
83+
# node whose cache mount was silently absent (cf. #876 self-heal regression).
84+
exec /proc/1/root/usr/bin/nsenter -t 1 -m -- /bin/sh -euc '
85+
HF=/mnt/hf_cache
86+
mkdir -p "$HF" /mnt/hf-cache-vfs
87+
88+
# rclone Bidirectional-mounts the parent /mnt, so /mnt must be an rshared
89+
# mountpoint for the FUSE to reach job pods. Bind only if it is not already
90+
# a mountpoint, and use rbind so an existing submount under /mnt is carried
91+
# into the new bind instead of being shadowed — a plain non-recursive bind
92+
# is what orphaned the old /mnt/hf_cache mount and wedged this init.
93+
mountpoint -q /mnt || mount --rbind /mnt /mnt
94+
mount --make-rshared /mnt || true
95+
96+
# Clear whatever occupies the live /mnt/hf_cache. A crashed rclone leaves a
97+
# dead FUSE (fails to stat); a prior run of this init can leave an xfs
98+
# self-bind (stats fine — a stat-gated loop would miss it). Peel a FUSE via
99+
# fusermount3 (the FUSE3 binary on AL2023 hosts; fusermount is not installed)
100+
# and anything else via umount -l, until the live path is no longer a
101+
# mountpoint. Bounded so it can never spin forever.
102+
i=0
103+
while mountpoint -q "$HF" && [ "$i" -lt 10 ]; do
104+
fusermount3 -uz "$HF" 2>/dev/null || umount -l "$HF" 2>/dev/null || break
105+
i=$((i + 1))
87106
done
88-
# rclone Bidirectional-mounts the parent /mnt, so /mnt must be a shared
89-
# mountpoint: bind it first, then keep /mnt/hf_cache a shared submount so
90-
# the FUSE still propagates to job pods.
91-
grep -q " /mnt " /proc/mounts || mount --bind /mnt /mnt
92-
mount --make-rshared /mnt
93-
grep -q " /mnt/hf_cache " /proc/mounts || mount --bind /mnt/hf_cache /mnt/hf_cache
94-
mount --make-rshared /mnt/hf_cache
107+
108+
# Re-establish /mnt/hf_cache as its own rshared submount. Guard on the live
109+
# path so a shadowed orphan in the mount table cannot make us skip the bind
110+
# (which left make-rshared to fail on a non-mountpoint and crash-loop).
111+
mountpoint -q "$HF" || mount --bind "$HF" "$HF"
112+
mount --make-rshared "$HF" || true
95113
'
96114

97115
containers:

0 commit comments

Comments
 (0)