Skip to content

worker: add option to mount cached input directories as read-only - #2751

Draft
dumbmoron wants to merge 2 commits into
TraceMachina:mainfrom
dumbmoron:mounts
Draft

dumbmoron wants to merge 2 commits into
TraceMachina:mainfrom
dumbmoron:mounts

Conversation

@dumbmoron

@dumbmoron dumbmoron commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Based on top of #2750, will need a rebase eventually.

What and why

Thousands of Chromium compile jobs use the same macOS SDK. NativeLink already caches a prepared copy of the SDK, but each job still creates its own directories and links, then deletes them afterward.

This PR adds experimental_readonly_input_mounts, which Linux workers can use to share selected cached directories through read-only mounts, avoiding that repeated setup and cleanup.

How was this verified?

Ran the same 128 real Chromium compile jobs for a macOS build, submitted from a Mac to four Linux workers running NativeLink v1.6.7 with this patch applied.

Configuration Cold run Warm run
Subtree cache, mounts disabled 256.3 s 72.2 s
SDK mounts 8.3 s 5.9 s

Before each cold run, the prepared directories were cleared but the downloaded files were kept. The warm run repeated the jobs without clearing anything. Both configurations used the same cache limits and concurrency, with prefetch enabled. All 512 compilations ran remotely, without action-cache hits, local fallback, or retries.

All four runs produced identical object files. Each configuration was measured once cold and once warm; these are batch timings, not full-build results.

Risk

Low when disabled; moderate when enabled. This option is intended for trusted builds whose jobs do not modify the selected inputs. The main risks are deleting a shared directory while a job still uses it, or keeping too many directories and running out of disk space. Tests cover keeping directories protected until job cleanup finishes.

Jobs with overlapping outputs or unsupported settings use normal input setup, but the overlap checks do not follow symlinks.


This change is Reviewable

After a directory construction fails, another caller may already be
waiting on its lock. Removing that lock from the map lets a new caller
create a different lock and construct the same cache path concurrently
with the waiting retry.

Keep the lock mapped while other callers hold references to it. Remove
it only when the map and the finishing caller are its sole owners, with
the map locked to prevent new callers from racing the reference check.

Add a regression using the existing get_or_create API that verifies a
new request waits for the retry's lock and cleanup removes the lock
after the final caller finishes.
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
nativelink Ready Ready Preview Sep 8, 2026 6:22pm UTC
nativelink-aidm Ready Ready Preview Sep 8, 2026 6:22pm UTC

Request Review

Chromium builds for macOS use the same Mac SDK across thousands of
compile jobs. The directory cache already shares the prepared SDK,
but each job still recreates its directories and links, then
deletes them afterward.

Add experimental_readonly_input_mounts so Linux workers can mount
selected cached directories read-only into each job instead.
This avoids repeated filesystem work for large, unchanged inputs
such as SDKs.

The option is disabled by default. Mounted directories stay in the
cache until job cleanup finishes, and incompatible jobs use normal
input setup.
@MarcusSorealheis

Copy link
Copy Markdown
Member

Ok ok, this one is a bit more involved. Give me a second to review

@erneestoc erneestoc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, the mechanics are carefully done. Notes below, most important first. Since #2750 merged on Sep 18 this needs a rebase; it still carries that commit.

What looks right

  • Inputs are bound after the namespace goes private and before the tmpfs masks the root, and the self-bind uses MS_REC so the submounts survive the mask. The test that puts the source under the masked root is a good one.
  • Absolute paths are resolved in the parent because std runs pre_exec after chdir.
  • The remount carries noexec/atime flags forward from statvfs, which a user namespace requires for locked mounts.
  • A failed mount aborts the spawn instead of running against an empty placeholder and caching a wrong result.
  • The cache pin outlives cleanup via the background drop task; the abandoned-action case is covered.
  • Eligibility is conservative: overlapping outputs, cwd inside a mount, .. escaping the root, and persistent workers all fall back.
  • Replacing the unwrap() in perform_remount with ? and not tombstoning a tree another action is mid-copy from are both improvements on their own.

1. The "mounts disabled" baseline does more work per action than the traditional path. I have not measured this, so please treat it as a request for a number rather than a claim about the magnitude. From directory_cache.rs: with experimental_subtree_caching on and a unique root per compile, every action is a root miss. construct_entry builds the root into a temp tree, during which create_subdirectory hits the SDK subtree and runs hardlink_directory_tree from the SDK entry into the temp root (walk 1). After the rename, construct_and_materialize runs hardlink_directory_tree again from the root entry into the workspace (walk 2). The root entry is then never reused and is eventually evicted, which is a third walk to delete it. The traditional path (directory_cache off) links each file once. So the 256 s / 72 s row is measuring the root cache's known cost on unique roots, not just the cost mounts remove. Could you add a row with directory_cache disabled, and a second sample per cell? The mount numbers may well still win, but that is the comparison an operator deciding whether to enable this needs.

2. When mounts apply, the directory cache is bypassed for the rest of the input tree. In inner_prepare, the mount branch calls download_to_directory_with_mounts directly and never reaches prepare_action_inputs_with_lease, so neither root nor subtree caching is used for the remaining inputs. For Chromium compiles that is probably fine, but any workload whose other inputs were getting cache hits regresses on those, and nothing documents that the two features are exclusive per action. Worth a sentence in the config doc, and possibly a follow-up that lets create_subdirectory leave a mount point for a selected path instead.

3. "Read-only" cannot hold against the action itself. The bind is created inside the action's own user namespace, so MNT_LOCK_READONLY never applies to it. The action runs as mapped root with CAP_SYS_ADMIN in that namespace and can mount -o remount,bind,rw the target and mutate the shared cache tree. Every later action on that worker using that digest then gets a corrupted input with a valid-looking action cache entry. The traditional path already exposes CAS blobs through hardlinks, so this broadens an existing exposure rather than creating a new class, but the doc string's "not a security boundary" undersells it: a single misbehaving action can silently poison results for every other action on the worker. Suggest saying that plainly, and as a follow-up consider dropping CAP_SYS_ADMIN from the bounding set before exec when mounts are active.

4. No fallback when prepare_for_mount fails. An error there propagates out of collect_download_links and fails the action. The cache path in prepare_action_inputs_with_lease warns and falls back to traditional download in the same situation. One retry without mounts would match existing behaviour.

5. Paths whose digest varies per action regress. A selected path that is not digest-stable gets a fresh cache construction per action, pinned until cleanup, which is strictly more work than the traditional path and thrashes the cache. The config doc should say that only digest-stable trees belong in the list.

6. No observability. There are no counters for mount hits, eligibility fallbacks, or prepare failures; in_use_entries is the only signal. An operator cannot tell whether the option is doing anything or how often it falls back.

7. Pinned entries ignore the eviction budget with no ceiling. evict_lru skips pinned entries, so several distinct pinned trees in flight can exceed max_size_bytes by their full size. Documented, but a pinned-bytes cap that falls back to traditional materialization would be safer than relying on disk headroom.

8. The namespace tests can pass without running. Both new tests eprintln! a skip message and return Ok when namespaces are unavailable; that output is captured, so the log shows ok either way. The Cargo job shows all five new tests as ok, but I can't tell from the log whether the two namespace tests executed. Also, no macos-26 job ran on this PR, so the non-Linux build is unverified by CI.

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.

3 participants