Skip to content

clearJobs() spins forever on FUSE mounts when a leaked fd blocks rmSync, permanently disabling auto-delete #584

Description

@FluffyDiscord

What

clearJobs() can enter an infinite loop that pins one CPU core permanently and silently disables auto-delete forever.

Observed on a container that ran 89 days at a constant 22.5% CPU (483 CPU-hours) with no conversions active and no ffmpeg running — only the bun main thread spinning, all worker threads idle.

Why

Two things combine:

1. src/pages/download.tsx:28 returns Bun.file(filePath). A read fd on the served file can outlive the response. Confirmed via /proc/<pid>/fd:

lr-x------ 1 root root 14 -> /app/data/output/<user>/<job>/<name>.h264.mp4 (deleted)

2. src/index.tsx:80 then runs rmSync(dir, { recursive: true, force: true }) on that directory.

On a FUSE filesystem (here Unraid's shfs at /mnt/user; same applies to sshfs/mergerfs), unlinking a file that still has an open fd does not remove the directory entry — FUSE renames it to .fuse_hidden<hex>. So rmSync never converges:

unlink → FUSE re-creates it as .fuse_hidden…rmdir fails ENOTEMPTY → readdir → unlink → …

The name is different on every pass:

$ ls -A output/<user>/<job>
.fuse_hidden0073584d3d30e9dd
.fuse_hidden0073584d3d30e9e0
.fuse_hidden0073584d3d30e9e2

Syscall sampling over the lifetime of the process shows only unlinkat (263) and openat (257).

Impact

clearJobs() reschedules itself on its own last line (src/index.tsx:93). Because it never returns, that timer is never armed — auto-delete is permanently dead for the life of the process, not just for the offending job.

In my case files from April were still present in July, including a stranded 111 MB upload. Only a container restart cleared it.

Reproduce

  1. Put /app/data on a FUSE mount.
  2. Convert a file and download it. (I could not pin down the exact condition that leaks the fd — I only observed the leaked read fd afterwards.)
  3. Wait AUTO_DELETE_EVERY_N_HOURS.

Suggested fix

  • Pass maxRetries to rmSync so the retry loop is bounded.
  • Wrap the per-job deletion in try/catch so one undeletable job can't block the others.
  • Move the setTimeout reschedule into a finally so a throw or hang can't permanently kill cleanup.
  • Explicitly close the Bun.file handle when the download response ends.

Versions

Observed on image sha256:e1f85be04bbaf8a55ead9261194c3ae0fa0957d303ea537127154860b2552afd. The relevant code is unchanged in current main — verified src/index.tsx:72-94 and src/pages/download.tsx:28 at v0.18.0.

Host: Unraid 7.2.4, /app/data bound to /mnt/user/appdata-array/convertx (shfs/FUSE).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions