Skip to content

feat(scala-sbt): warm-cache plugin command; add ADR-015#21

Merged
razvanz merged 11 commits intomainfrom
docs/adr-015-no-hot-cache-mounts
Apr 27, 2026
Merged

feat(scala-sbt): warm-cache plugin command; add ADR-015#21
razvanz merged 11 commits intomainfrom
docs/adr-015-no-hot-cache-mounts

Conversation

@razvanz
Copy link
Copy Markdown
Owner

@razvanz razvanz commented Apr 27, 2026

Closes #18.

ADR-015: avoid virtiofs for churning caches. virtiofsd --cache=auto accumulates FDs monotonically; PR #20 raised the ceiling but didn't change the trajectory.

In-tree plugins seed warm host caches via a host-side plugin command invoked from post-up, mirroring nixbox aws login / nixbox claude-code sync-config. Third-party plugins aren't bound — the FD trade-off is documented.

plugins/scala-sbt: new nixbox scala-sbt warm-cache streams ~/.cache/coursier and ~/.ivy2 from host into the guest via tar | nixbox run "tar -x", sentinel-guarded. No virtiofs mounts.

Test plan

  • shellcheck, bats (40/40), nix-eval (32/32)
  • resolved scala-sbt config: zero virtiofs mounts, post-up calls warm-cache
  • e2e — KVM-only, deferred to CI

🤖 Generated with Claude Code

razvanz and others added 4 commits April 27, 2026 10:29
Closes #18.

PR #20 raised virtiofsd's NOFILE ceiling 8x but virtiofsd --cache=auto
accumulates backing-file FDs monotonically; any session long enough hits
the new ceiling. The durable fix is to keep churning caches off virtiofs
entirely.

ADR-015 codifies the rule: plugins must not virtiofs-mount package-manager
caches, build caches, or other LRU-shaped directories. Cache state lives
on root.img (which persists per workspace), at the tool's default location
or via env var redirect from a setup script.

scala-sbt migrated as the reference: dropping the ~/.cache/coursier and
~/.ivy2 mounts is sufficient since both defaults are under \$HOME on
root.img. Trade-off: first sbt update per workspace re-fetches; cache then
stays warm for the workspace lifetime.

Future work (called out in the ADR): VM snapshotting via qcow2 backing or
cloud-hypervisor save/restore would obviate the warmup cost. Separately
scoped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Credentials are not virtiofs-mounted — they're written to a one-shot ext4
disk that the guest reads at boot and unmounts (ADR-003). Listing
~/.env as a virtiofs example contradicts the architecture and the
explicit-credentials philosophy (ADR-013).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
~/.ssh is not a recommended virtiofs target — the architecture explicitly
avoids sharing host SSH identity. ADR-014 injects a per-VM key for VM
SSH identity, and host SSH keys (GitHub, AWS, deploy keys) deliberately
do not cross the sandbox boundary.

Reframe the appropriate-virtiofs-uses paragraph to state the principle
(moderate file count, in-place cross-boundary semantics) and point at
dedicated channels for sensitive data: ADR-003 (credentials disk),
ADR-014 (SSH key injection). Plugins mounting additional host state
(claude-code/~/.claude) own that tradeoff in their own ADR (ADR-010).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-015 distinguishes steady-state runtime virtiofs use (forbidden for
churning caches) from bounded one-shot bootstrap reads (permitted).

scala-sbt adopts the bootstrap pattern:
- RO virtiofs mounts at /mnt/host-cache/coursier and /mnt/host-cache/ivy2
  (only if host paths exist)
- Setup script rsyncs into ~/.cache/coursier and ~/.ivy2 on first boot,
  guarded by a sentinel file
- Subsequent boots: rsync is a no-op; sbt's runtime I/O lives entirely
  on root.img and never crosses virtiofs
- rsync added to nix.packages

Trade-off: virtiofsd holds backing-file FDs accumulated during the rsync
pass for the VM's lifetime (~50-100k for typical coursier caches), well
under the 524288 ceiling from PR #20. No further accumulation occurs
since nothing reads the mount after warmup.

ADR-015 updated to codify the bootstrap exception with explicit MUST
requirements (readonly, side path, sentinel-guarded, no post-warmup
access).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@razvanz razvanz changed the title docs(adr): no virtiofs for hot caches; migrate scala-sbt feat(scala-sbt): bootstrap host caches at boot; add ADR-015 Apr 27, 2026
razvanz and others added 3 commits April 27, 2026 10:51
Remove cruft that doesn't drive the decision:
- toolchain reference table (belongs in a plugin-authoring guide)
- "what stays on virtiofs" sub-section (scope creep into ADR-003/014/010)
- atomic-ops rationale (collapsed; ADR-001 owns it)
- host-cache-sharing trade-off discussion (bootstrap addresses it)
- "log directories" example (logs are append-mostly, not LRU)
- future-work detail (one-line callout)

Keep what matters for a plugin author reading this in a year: the
steady-state prohibition, the bootstrap MUSTs, the FD rationale, and
scala-sbt as concrete grounding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bootstrap mounts (RO virtiofs at /mnt/host-cache/*) were left attached
for the VM's lifetime, leaving virtiofsd processes idle but holding the
backing-file FDs accumulated during the rsync. ADR-015's whole pitch is
"no steady-state virtiofs"; leaving virtiofsd idling walks back from
that.

Changes:
- bin/nixbox: do_unmount is idempotent — if no active mount matches the
  target, log and return 0 instead of dying. Lets post-up hooks run
  unconditionally on every up (warmup is sentinel-guarded; mount may
  not have been (re)created on subsequent boots).
- plugins/scala-sbt: post-up hook releases /mnt/host-cache/{coursier,ivy2}
  via nixbox unmount. nixbox unmount detaches the virtio device and
  kills virtiofsd, freeing the accumulated FDs.
- ADR-015: split the bootstrap rules into MUSTs (readonly, side path,
  sentinel, no post-warmup access) and SHOULDs (release after warmup).
  In-tree plugins MUST release; third-party plugins are encouraged but
  not required, since a single bootstrap's FDs are well under the
  524288 ceiling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the virtiofs-bootstrap pattern with a host-side plugin command
following the established 'nixbox aws login' / 'nixbox claude-code
sync-config' pattern.

- New: plugins/scala-sbt/commands/warm-cache.sh — pipes host's
  ~/.cache/coursier and ~/.ivy2 into the guest via 'tar | nixbox run
  "tar -x"' over the existing SSH channel. Sentinel-guarded for
  idempotency.
- Plugin: post-up hook calls 'nixbox scala-sbt warm-cache'. No virtiofs
  mounts at all. No rsync in nix.packages. Setup script reverts to
  credentials-only.
- ADR-015: drops the bootstrap-exception entirely. The decision is now
  strictly "no virtiofs for churning caches"; warmup is delivered by a
  host-side plugin command, not a transient mount.
- bin/nixbox: revert the do_unmount idempotency change — it was
  scaffolding for the previous post-up unmount approach, no longer
  needed.

Trade-off vs. the previous virtiofs-bootstrap approach: tar over SSH is
slightly slower than virtiofs+rsync (~1.5x for local vsock) but
eliminates the virtiofsd FD legacy entirely — no idle processes, no
held FDs, no exception in the ADR. The plugin command is also
user-invokable for manual refreshes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@razvanz razvanz changed the title feat(scala-sbt): bootstrap host caches at boot; add ADR-015 feat(scala-sbt): warm-cache plugin command; add ADR-015 Apr 27, 2026
razvanz and others added 4 commits April 27, 2026 11:12
Reframe the ADR so it documents trade-offs rather than dictating to
third-party plugin authors:

- Title: "No virtiofs ..." → "Avoid virtiofs ..."
- Decision: "Plugins MUST NOT ..." → "In-tree plugins do not ..."
- Add an explicit paragraph stating that third-party plugins are not
  bound; authors who virtiofs-mount churning caches accept the
  FD-pressure cost documented in #18
- Plugin-command pattern: presented as one clean alternative, not "the
  right primitive"
- Consequences scoped to in-tree plugins

The substance — what the project does and why, and what it costs to do
otherwise — is unchanged. Only the framing softens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cut the Rationale section entirely — its FD-monotonic argument is
already in Problem; the plugin-command motivation is conveyed by the
"established pattern" cue in Decision. Compress Consequences to two
bullets.

36 → 27 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Future work" implied roadmap commitment. Reframe as a possible
direction the trade-off could be revisited from, with explicit "not
committed work" framing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ions

- scala-sbt provides line: include scala-cli (added in #15) and the
  warm-cache plugin command. Mention root.img persistence + ADR-015
  link in the trailing paragraph.
- Known limitations: add "No live host↔guest cache sharing" entry
  pointing at ADR-015 — useful for users who'd otherwise expect
  virtiofs-style mount semantics for caches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements a new approach for Scala/SBT caches that avoids virtiofs for churn-heavy directories by seeding guest-local caches from the host via a plugin command, and documents the rationale in a new ADR.

Changes:

  • Removes virtiofs mounts for ~/.cache/coursier and ~/.ivy2 from the scala-sbt plugin and adds a post-up hook to warm caches.
  • Adds nixbox scala-sbt warm-cache to stream host caches into the guest (sentinel-guarded to be idempotent).
  • Adds ADR-015 documenting the “no virtiofs for hot caches” decision and updates READMEs accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
plugins/scala-sbt/default.nix Drops cache mounts; adds hooks.post-up to invoke warm-cache automatically.
plugins/scala-sbt/commands/warm-cache.sh New plugin command that streams coursier/ivy2 caches into the guest via `tar
plugins/scala-sbt/README.md Updates plugin docs for new cache behavior and new warm-cache command.
docs/decisions/015-no-virtiofs-hot-caches.md New ADR capturing the motivation/decision/trade-offs.
README.md Updates scala-sbt plugin overview and adds a “no live cache sharing” limitation note.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/scala-sbt/commands/warm-cache.sh
@razvanz razvanz merged commit 02fbf0f into main Apr 27, 2026
6 checks passed
@razvanz razvanz deleted the docs/adr-015-no-hot-cache-mounts branch April 27, 2026 11:09
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.

Too many files open

2 participants