- Go 97.6%
- Dockerfile 2.4%
Injection goroutines LockOSThread + never unlock, but Go's runtime doesn't reap the tainted thread synchronously. If the main goroutine gets scheduled onto a tainted thread before it's destroyed, reads of /proc/self/mountinfo return the consumer's ns view (which doesn't contain the host's FUSE path), causing false "not mounted" reports even though the actual host mount is fine. Fix: LockOSThread at main() startup pins the reconcile loop to the initial thread (M0), which is never mutated. Injection goroutines still spawn and die on other threads; the tainted-thread leak still happens but no longer affects the main loop's view. Verified: full cycle (decypharr recycle → 7 stale ns detected → heal) now stays "all consumers healthy" indefinitely after the heal. |
||
|---|---|---|
| deploy | ||
| .gitignore | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| README.md | ||
mount-refresher
Heals stale FUSE mount references in consumer container mount namespaces after the FUSE producer (decypharr) recycles.
Problem
The FUSE mount at /var/lib/decypharr-dfs on the host lives in a kernel
peer group created at mount time. When decypharr's container dies, the
peer group is destroyed. Consumer containers (sonarr, radarr, jellyfin, …)
with HostToContainer (rslave) mounts to that path retain a dangling
slave reference to the dead group. A new decypharr FUSE mount forms a
fresh peer group — but consumer mounts can't join it, because propagation
relationships are established only at mount creation time.
Result: after a decypharr recycle, every consumer sees Socket not connected on /mnt/decypharr and stays broken until pod restart.
Approach
Uses open_tree(2) + move_mount(2) (Linux 5.2+) to:
- Clone the fresh FUSE mount subtree from the host's mount namespace
- Enter each stale consumer's mount namespace via
setns(2) - Lazy-detach the stale mount (
MNT_DETACH) - Attach the cloned mount at the target path
Result: each consumer's /mnt/decypharr points at the fresh FUSE with
no process restart, no pod restart, no init-hook rerun.
Runs as a DaemonSet on the node where decypharr lives (Arc node in
our case). Detects stale mounts by scanning /host/proc/*/mountinfo
for fuse.decypharr entries whose major:minor differs from the host's
current mount.
Gotchas
- Go multithreading breaks
setns(CLONE_NEWNS): the kernel refuses becausefs_structis shared across goroutines' OS threads. Weunshare(CLONE_FS)on the calling thread first to satisfy the check, then tag the thread as tainted (neverUnlockOSThread) so the Go runtime discards it after the goroutine exits. - Requires
privileged: true+hostPID: true:open_treeneedsCAP_SYS_ADMIN; ns fds need hostPID to see other pods' processes.
Deployment
DaemonSet on the Arc node. See deploy/daemonset.yaml.