mirror of
https://github.com/kvcache-ai/AgentENV.git
synced 2026-11-05 01:17:36 +00:00
[PR #261] fix(sandbox): shard the persistent serial log directory #298
Labels
No labels
bug
documentation
enhancement
enhancement
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/AgentENV#298
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/kvcache-ai/AgentENV/pull/261
Author: @makhov
Created: 9/7/2026
Status: 🔄 Open
Base:
main← Head:fix/shard-serial-log-dir📝 Commits (2)
53e4061fix(sandbox): shard the persistent serial log directory211c703fix(sandbox): shard the persistent serial log directory. review fixes📊 Changes
7 files changed (+68 additions, -9 deletions)
View changed files
📝
docs/src/configuration/env-vars.md(+1 -1)📝
docs/src/configuration/reference.md(+1 -1)📝
docs/src/internals/persistence-artifact-inventory.md(+2 -2)📝
docs/src/internals/sandbox-testing.md(+3 -2)📝
src/cfg.rs(+2 -1)📝
src/sandbox/firecracker/config.rs(+2 -1)📝
src/sandbox/firecracker/sandbox.rs(+57 -1)📄 Description
What
Insert one shard directory level between the persistent serial-output root and each sandbox's own log directory, so the layout becomes
{serial_dir}/{shard}/{sandbox_id}/, where the shard is the last two hex digits of the sandbox id.Why
Serial output is deliberately persistent:
{serial_dir}/{sandbox_id}/outlives the sandbox so its boot log can be read after the fact. Nothing prunes it, soon a busy node the root accumulates one child per sandbox ever created, at ~470 entries/sec under sustained create load. Nearly every child holds a single zero-byte
firecracker-stderr.log— a guest resumed from a snapshot writes nothing to the console.That cost is paid by every create, not by whoever reads a log: each create links a new entry into the root, under an exclusive lock on the parent inode.
At ~594k entries a node sustained 197–280 creates/sec; with the root emptied and nothing else changed, the same node sustained 460–475 creates/sec at 76–78% CPU — about 1.9× on create throughput, on a node that had simply been up a while. The inverse correlation between root size and peak create rate held across all four nodes measured.
The effect is invisible on a fresh node and grows silently with uptime, which is why it seems worth fixing in the layout rather than in an operator runbook.
Related issue
Closes #260
Scope and non-goals
Included:
FirecrackerSandbox::default_log_dir;serial_shardhelper with the rationale for the shard key;nil-id edge case;
Non-goals, deliberately:
Design and behavior changes
default_log_dirgains onejoin(serial_shard(self.id))before the per-sandbox component. All three writers of that path —firecracker_stdout_path,firecracker_stderr_path, andfirecracker_log_path— already route throughdefault_log_dir, so one edit covers every producer, and the directory is created by the same code that created it before.The shard is the id tail, not its head. Sandbox ids are UUIDv7, whose leading bytes are a millisecond timestamp: every sandbox created in the same period shares a prefix, so head-sharding would put an entire burst — exactly the load that causes the problem — into one bucket. The trailing bytes are random. Minting 411,340 ids inside a single millisecond and sharding them both ways gives 256 buckets of at most ~1.7k entries on the tail, against one bucket holding all 411,340 on the head.
serial_shard_spreads_ids_created_togetheristhe regression test for that property.
Two hex digits (256 buckets) is sized for the observed regime: it holds the measured ~600k directories at a few thousand children each, well inside the range where directory insertion is not the bottleneck.
Compatibility and operations
serial_dir/AENV_FIRECRACKER_SERIAL_DIRkeep their meaning; only the layout beneath the root changes. Explicitstdout_path/stderr_pathoverrides bypass this path entirely and are unaffected.{serial_dir}/{sandbox_id}— every use ofserial_output_base_dirandserial_diris either this computation or config plumbing — so the change is self-contained in-tree. Out-of-tree tooling that globs one level deep is the one thing that breaks; see risks.Validation
make fmtmake clippy— ran the narrower equivalent for the touched crate, see belowmake test-unit— ran the touched tests only, see belowmake -C services test(required whenservices/changes)maketargetCommands and results:
Skipped checks and reasons:
make clippyandmake test-unitin full: not run. I ran clippy for the crate I touched (-p agentenv --lib --all-features -- -D warnings, clean) and the three new tests, rather than the whole workspace and the full unit-test target — the latter also drivesuvm-ublkand the ignored capability tests, which want a configured ublk host and root. Happy to post a full run if you would rather see one before reviewing; CI will cover it in any case./dev/kvm, and network namespaces, which I do not have available for this change. The touched code is a pure path computation with no privileged behavior, and the three writers that consume it are unchanged.make -C services test: nothing underservices/changes.Happy to run something more specific if you want it.
Risks and reviewer notes
{serial_dir}/{sandbox_id}/. Anything globbing one level deep silently finds nothing after this change rather than erroring. If you would rather this be opt-in behind a config key, or want it called out in release notes, I am happy to add either.serial_shardfalls back to"00"for an id whose string form has fewer than two hex digits, which is unreachable for a parsed UUID; the alternative was a panic on a logging path.src/sandbox/firecracker/sandbox.rs— the one-line change indefault_log_dirand theserial_shardhelper below it.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.