Skip to content

fix(read): bound remote chunk fetch with a timeout to prevent FUSE thread-pool wedge#200

Merged
XciD merged 1 commit into
mainfrom
fix/read-fetch-timeout
Jun 29, 2026
Merged

fix(read): bound remote chunk fetch with a timeout to prevent FUSE thread-pool wedge#200
XciD merged 1 commit into
mainfrom
fix/read-fetch-timeout

Conversation

@XciD

@XciD XciD commented Jun 29, 2026

Copy link
Copy Markdown
Member

Problem

After a number of reads, all cold reads on the mount freeze silently. head -c 5MB on a not-yet-read file returns 0 bytes and hangs until timeout, with no error in the daemon log. ls still responds (metadata, no data fetch). A fresh daemon re-reads everything fine, proving it is accumulated daemon state, not the network/file/token.

Observed in production under media streaming (Jellyfin over an hf-mount bucket): ffmpeg probes, seeks (each opens a new range download) and client retries generate many cold fetches that get abandoned.

Root cause

Each FUSE read() runs on a worker thread blocked in runtime.block_on(virtual_fs.read(...)), and the session runs with n_threads = max_threads (default 16). The read path's fetch_data awaited stream.next() with no timeout; the MAX_ATTEMPTS retry loop only caught Err/EOF, never a hang. A stalled fetch (client-aborted seek, hung CDN socket) parked its worker forever (block_on is not cancellable). Once enough piled up, all 16 worker threads were wedged and the mount stopped serving cold reads. readdir kept working (cached metadata, resolves instantly). Nothing was logged because the await never returned.

This path is not gated on write mode — reads of clean remote files go through the lazy prefetch (open_lazyfetch_data) in every mode, so this is the fix for the reported wedge regardless of --advanced-writes.

Fix

  • New --read-fetch-timeout-ms flag (default 30000, 0 disables), plumbed MountOptionsVfsConfigVirtualFs.
  • fetch_data bounds each chunk read with tokio::time::timeout. On expiry it logs an error! (breaking the silent hang), fails the attempt, and lets the retry loop return EIO. Dropping the stream cancels the in-flight request.

Test

read_stalled_stream_times_out_with_eio: a mock stream that hangs on next() is read through a VFS with a 150ms fetch timeout; the read returns EIO instead of hanging. An outer tokio::time::timeout guard makes a regression (unbounded wait) fail deterministically. Verified that with the bound disabled (Duration::ZERO, i.e. the pre-fix code path) the test fails with Elapsed, confirming it reproduces the wedge.

Scope

This PR is just the read-path fix — the reported wedge. Complementary follow-ups, split out so this stays minimal and mergeable:

…read-pool wedge

Each FUSE read() runs on a worker thread blocked in block_on on a
synchronous CAS/CDN fetch. fetch_data awaited stream.next() with no
timeout, so a stalled connection (client-aborted media seek, hung CDN
socket, killed ffmpeg probe) parked that worker thread forever. The
MAX_ATTEMPTS retry loop only caught errors and EOF, never a hang.

Once enough stalled reads accumulated, all max_threads (default 16)
workers were wedged and the mount silently stopped serving cold reads
while readdir kept working (served from cached metadata on a thread
that resolves instantly). No error surfaced because the await never
returned.

Bound each chunk read with a configurable timeout (--read-fetch-timeout-ms,
default 30s, 0 disables). On expiry we log an error (breaking the
silence), fail the attempt, and let the retry loop eventually return
EIO. Dropping the stream on the way out cancels the in-flight request.

Note: the sparse-write read path (fill_sparse_holes) has the same
unbounded await and should get the same treatment when that branch lands.
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

============================================================
  Benchmark — 50MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                    260.9 MB/s     109.9 MB/s
  Sequential re-read                2112.0 MB/s    2440.1 MB/s
  Range read (1MB@25MB)                0.5 ms         0.2 ms
  Random reads (100x4KB avg)           0.0 ms         0.0 ms
  Sequential write (FUSE)           1347.9 MB/s
  Close latency (CAS+Hub)            0.127 s
  Write end-to-end                   305.5 MB/s
  Dedup write                       1607.6 MB/s
  Dedup close latency                0.100 s
  Dedup end-to-end                   382.2 MB/s
============================================================
============================================================
  Benchmark — 200MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                    843.5 MB/s     891.5 MB/s
  Sequential re-read                2214.9 MB/s    2382.2 MB/s
  Range read (1MB@25MB)                0.2 ms         0.2 ms
  Random reads (100x4KB avg)           0.0 ms         0.0 ms
  Sequential write (FUSE)           1677.1 MB/s
  Close latency (CAS+Hub)            0.147 s
  Write end-to-end                   750.7 MB/s
  Dedup write                       1795.6 MB/s
  Dedup close latency                0.109 s
  Dedup end-to-end                   905.8 MB/s
============================================================
============================================================
  Benchmark — 500MB
------------------------------------------------------------
  Metric                                 FUSE          NFS
  ------------------------------ ------------ ------------
  Sequential read                   1492.9 MB/s    1217.8 MB/s
  Sequential re-read                2175.3 MB/s    2354.4 MB/s
  Range read (1MB@25MB)                0.3 ms         0.2 ms
  Random reads (100x4KB avg)           0.0 ms         0.0 ms
  Sequential write (FUSE)           1550.8 MB/s
  Close latency (CAS+Hub)            0.117 s
  Write end-to-end                  1138.5 MB/s
  Dedup write                       1573.2 MB/s
  Dedup close latency                0.268 s
  Dedup end-to-end                   853.4 MB/s
============================================================
============================================================
  fio Benchmark Results
------------------------------------------------------------
  Job                        FUSE MB/s   NFS MB/s  FUSE IOPS   NFS IOPS
  ------------------------- ---------- ---------- ---------- ----------
  seq-read-100M                  500.0      469.5                      
  seq-reread-100M               2325.6      348.4                      
  rand-read-4k-100M                0.1        0.1         17         18
  seq-read-5x10M                 847.5      847.5                      
  rand-read-10x1M                  0.1        0.1         37         36
  Random Read Latency           FUSE avg      NFS avg
  ------------------------- ------------ ------------
  rand-read-4k-100M           58925.8 us   54530.5 us
  rand-read-10x1M             27377.9 us   27719.9 us
============================================================

@github-actions

Copy link
Copy Markdown
Contributor

POSIX Compliance (pjdfstest)

============================================================
  pjdfstest POSIX Compliance Results
------------------------------------------------------------
  Files: 130/130 passed    Tests: 832 total (0 subtests failed)
  Result: PASS
------------------------------------------------------------
  Category               Passed    Total   Status
  -------------------- -------- -------- --------
  chflags                     5        5       OK
  chmod                       8        8       OK
  chown                       6        6       OK
  ftruncate                  13       13       OK
  granular                    5        5       OK
  mkdir                       9        9       OK
  open                       19       19       OK
  posix_fallocate             1        1       OK
  rename                     10       10       OK
  rmdir                      11       11       OK
  symlink                    10       10       OK
  truncate                   13       13       OK
  unlink                     11       11       OK
  utimensat                   9        9       OK
============================================================

@XciD XciD force-pushed the fix/read-fetch-timeout branch 3 times, most recently from c9c4c73 to ec823c1 Compare June 29, 2026 09:01
@XciD XciD marked this pull request as ready for review June 29, 2026 11:47
@XciD XciD force-pushed the fix/read-fetch-timeout branch 2 times, most recently from 1c050f2 to 1b1edab Compare June 29, 2026 12:23
@XciD XciD merged commit c42ead1 into main Jun 29, 2026
12 checks passed
@XciD XciD deleted the fix/read-fetch-timeout branch June 29, 2026 13:58
XciD added a commit that referenced this pull request Jun 30, 2026
…204)

## Context

Follow-up to #200. A reviewer on the xet-core side
([xet-core#880](huggingface/xet-core#880))
pointed out that the xet client already has the correctly-grained stall
guard: `HF_XET_CLIENT_READ_TIMEOUT`, a **per-read inactivity timeout
that resets on every byte received** (so slow-but-progressing transfers
are unaffected; only a genuine stall trips it).

We had it set to **600s**, raised long ago "for shard uploads". But
uploads now use a separate client with **no** read_timeout
(`shard_upload_http_client` / `build_auth_http_client_no_read_timeout`),
so that 600s only governs the **download/reconstruction** path. A
stalled CAS/CDN read therefore wouldn't error for 10 minutes, which is
what let stalled reads pile up and wedge the mount.

## Change

Drop `HF_XET_CLIENT_READ_TIMEOUT` to **30s**.

- #200's per-chunk timeout already bounds the streaming **read** path
(`fetch_data`).
- This additionally bounds the **whole-file download** path
(`download_to_file`, used by advanced-write open / flush), which goes
straight through xet-core's `download_file` and was bounded only by this
value. That makes the separate write-path workaround in #203 unnecessary
— closing it.

Healthy transfers are unaffected (the timeout resets on progress). Still
overridable via the env var.

## Layering note

This is the fix at the right layer (the client), as the xet-core
reviewer recommended. #200's per-chunk wrapper stays as complementary
defense-in-depth: it also catches non-socket stalls (e.g. a stall inside
the reconstruction pipeline) that a socket read_timeout would not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant