Skip to content

fix(server): zero-copy in-memory artifact cache to kill PXE-boot starvation - #36

Merged
Zorlin merged 1 commit into
feat/metrics-endpointfrom
fix/artifact-cache
Jun 30, 2026
Merged

fix(server): zero-copy in-memory artifact cache to kill PXE-boot starvation#36
Zorlin merged 1 commit into
feat/metrics-endpointfrom
fix/artifact-cache

Conversation

@Zorlin

@Zorlin Zorlin commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

Under concurrent PXE boot, N machines pull the same large boot assets (modloop
~295 MiB, initramfs, vmlinuz, apkovl — served from /boot/{arch}/* local files)
at once. read_file_as_stream served each from disk independently, so N clients
re-read the same file N times off one PVE-backed store (loop0 over
vm-1000-disk-0.raw). Disk throughput collapsed and stalled every consumer — a
bimodal 2-fast (~87s) / 3-slow (~280s) straggler pattern across the 5-node
test-k8s fleet.

Root cause

Investigation traced every serve path. The "scary" ones turned out to be dead
code (0 fires across months of journal):

  • stream_download_with_caching — a no-single-flight pullthrough proxy with the
    exact N×-re-fetch flaw, but the iPXE script (api.rs:2575-2587) points clients
    at /boot/{arch}/* local files, not the /ipxe-artifacts proxy.
  • the Range-branch read_to_end-into-a-Vec buffering (0 RANGE_READ).
  • AppState.client_ip (never written), track_download_progress (gated off).

The live culprit was the local-file path itself: lock-free and correct per
request, but re-reading the same artifact from disk once per concurrent client.

Fix

ArtifactCache on AppState (new module artifact_cache.rs):

  • Load each file into memory once into a single shared Bytes. Population is
    single-flight (per-path tokio::Mutex + double-check): the cold read
    happens once; concurrent readers await, then slice the shared buffer (no
    duplicate loads, no torn reads).
  • Every client streams Bytes::slice zero-copy views of that one allocation
    (refcounted — CoW-on-write, never written) in 64 KiB chunks. One disk read, N
    independent seeks.
  • Bounded LRU (default 1 GiB; DRAGONFLY_ARTIFACT_CACHE_BYTES). Files over
    the cap return TooLarge and the caller streams them from disk unchanged.
  • read_file_as_stream serves from the cache when AppState is available and
    the file fits (early-return on Cached, disk fallback on TooLarge/error).
    This also unifies the range + full branches for cached files: range
    requests slice the shared buffer instead of read_to_end into a per-request
    Vec.
  • Wired serve_boot_asset (the live /boot/{arch}/* path), serve_cached_image,
    and serve_os_image.

Verification

Same 5-box boot storm, before → after deploying the fix:

metric before after
parallel wall-clock 282.3s 112.3s (2.5×)
straggler 282s 112s
straggler gap 3.2× 1.4×
the 3 starved boxes 246–282s 77–112s
boot plane (/metrics) (seconds of disk) 35 reqs / 0.447s

The lone 112s straggler is the single cold cache load (modloop's first disk
read, the 368 ms max in /metrics); every box after serves from RAM. RSS 366
MiB with the cache warm, well under the 1 GiB cap.

Tests

5 new artifact_cache tests: load-once-then-memory, single-flight (16
concurrent callers share one allocation — proven by Bytes pointer equality),
zero-copy slice, LRU eviction, oversized-not-cached. dragonfly-server lib
198/198 green; fmt clean; no new clippy warnings.

Notes

  • Cached boot assets are served from RAM until LRU-evicted/restart — correct for
    a boot storm (assets are immutable mid-storm); a re-staged modloop needs a
    restart to be picked up.
  • Stacked on feat(server): /metrics endpoint + HTTP request metrics #35 (metrics). Retargets to main automatically when that merges.

🤖 Generated with Claude Code

…reads

Under concurrent PXE boot, N machines pull the same large artifact (modloop
~295 MiB, initramfs, vmlinuz, apkovl) at once. read_file_as_stream served each
from disk independently, so N concurrent clients re-read the same file N times
off one PVE-backed store (loop0 over vm-1000-disk-0.raw) — disk throughput
collapsed and stalled every consumer. The intended single-flight pullthrough
(stream_download_with_caching) is dead code (0 fires; the iPXE script points
clients at /boot/{arch}/* local files, not /ipxe-artifacts), so it never
deduped the reads.

Add ArtifactCache (on AppState): load each file into memory ONCE into a shared
Bytes; every concurrent client streams Bytes::slice zero-copy views of that one
allocation (refcounted — CoW-on-write, never written) in 64 KiB chunks.
Population is single-flight (per-path tokio::Mutex, double-checked) so the cold
read happens once and concurrent readers await then slice the shared buffer —
no duplicate loads, no torn reads. Bounded LRU (default 1 GiB,
DRAGONFLY_ARTIFACT_CACHE_BYTES); files over the cap stream from disk unchanged.

read_file_as_stream now serves from the cache when AppState is available and
the file fits (early-return on Cached, fall through to the existing disk path
on TooLarge/error). This also unifies the range + full branches for cached
files: range requests slice the shared buffer instead of read_to_end into a
per-request Vec — the "well-designed range" the per-request buffering was
supposed to be. Wired serve_boot_asset (already passed Some(state)),
serve_cached_image, and serve_os_image (signature + the /os route closures) to
reach the cache.

Tests (artifact_cache, 5): load-once-then-memory, single-flight (16 concurrent
callers share one allocation — proven by Bytes pointer equality), zero-copy
slice, LRU eviction, oversized-not-cached. dragonfly-server lib 198/198 green;
fmt clean; no new clippy.

Co-Authored-By: Claude <noreply@anthropic.com>
@Zorlin
Zorlin merged commit df45084 into feat/metrics-endpoint Jun 30, 2026
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