perf(storage): ext4 metadata volume for fsync-hot boltdb state (ABX-496) - #497
Conversation
The default (and Rosetta) machine gains a 2 GiB sparse docker-meta.img attached as vdc. Existing machines are recreated via a new block_devices drift arm so upgrades pick up the disk; image files are untouched by recreation. arcbox disk usage reports the paired image.
… volume (ABX-496) Copy via .partial + atomic rename, retire the btrfs-side source to *.pre-ext4, and leave an empty mountpoint stub. Every crash window re-converges; a blank volume never resurrects retired state. Pure std::fs so the tests run on any host.
…s (ABX-496) Formats /dev/vdc (journaled ext4 + fast_commit) on first use, mounts it at /run/arcbox/metadata, migrates the five profiled hot entries, and binds them before containerd/dockerd start. Device absent or mkfs missing on a blank disk degrade to the btrfs-only layout; a present but unmountable volume hard-fails (e2fsck -y retry first) so dockerd never boots against stale shadowed state.
Measured no fsync benefit (9.5 ms nocow vs 10.5 ms cow — the cost is the btrfs commit path, not COW), the boltdb dirs it targeted now live on the ext4 metadata volume, and the inherited flag on the snapshotter dir silently disabled zstd compression for every extracted layer file.
… probing (ABX-496) The host injects arcbox.docker_metadata_device=/dev/vdc when it attaches the disk. Declaration is authoritative: declared-but-missing hard-fails instead of silently skipping, and undeclared boots (older daemon, bare hv_e2e probe) skip with zero probe delay instead of a 5 s timeout. An already-present default node without a declaration is still honored for bespoke e2e configs.
… (ABX-496) A/B on the same guest layout and host: a guest fsync costs ~10 ms under the default .full mode vs ~2 ms under .fsync and ~2 ms on the custom-HV backend, whose block worker has always used plain fsync for guest FLUSH. .fsync aligns both backends on the same power-loss window and matches the durability level Colima/OrbStack ship; measured guest fsync drops 10.3 -> 1.7 ms on the ext4 metadata volume.
…Z sync-mode finding
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 101ac9bc1c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
ℹ️ No critical issues — a few minor observations inline, all non-blocking.
Reviewed changes — a 2 GiB ext4 metadata volume (vdc) is added to carry the five fsync-hot boltdb locations, bind-mounted over their btrfs paths before containerd/dockerd start, plus a VZ synchronization-mode switch that removes the F_FULLFSYNC cost on every guest flush.
- Crash-safe migration state machine —
metadata_migrate::prepare_entrycopies source→.partial→atomic-rename, retires the btrfs source to*.pre-ext4, and stubs the mountpoint; idempotent with 8 crash-window tests. - Guest mount/format orchestration —
metadata_volume::ensure_metadata_mountwaits for the declared device, formats ext4 (fast_commit, no lazy init),e2fsck -yretry on mount failure, then binds each mapping. - Third block device + drift —
create_default_machineattaches vdc and declaresarcbox.docker_metadata_device=on the cmdline; a newblock_devices-count drift arm recreates pre-vdc machines. - Drop btrfs NOCOW flagging — the old
disable_cow_on_metadata_dirspath is removed; the hot metadata now lives on ext4 and the rest stays on plain btrfs. - VZ
.fsyncsynchronization —VZDiskImageStorageDeviceAttachmentswitches from the default.fullto.fsync, aligning VZ durability with HV/Colima. - Supporting wiring —
docker_meta_img_path(), disk-usage reporting, resource-cleanup image list (4 names), constants, docs, andassets.lockbump to v0.6.11.
ℹ️ Blank recreated metadata volume starts fresh with no warning
When the metadata image is deleted and recreated blank while a *.pre-ext4 backup still exists on btrfs, prepare_entry correctly refuses to resurrect the stale backup and starts fresh (blank_volume_after_retire_starts_fresh_not_stale). That is the right behavior, but the runtime then opens an empty database silently — no log line records that a .pre-ext4 recovery artifact was left behind and skipped.
Given the design explicitly treats the two images as a paired set (delete/move together), an operator who accidentally removed only docker-meta.img gets a fresh-but-empty metadata store against populated bulk data with nothing in the log to explain it.
Technical details
# Blank recreated metadata volume starts fresh with no warning
## Affected sites
- `guest/arcbox-agent/src/metadata_migrate.rs:82-84` — the `create_entry(&final_path, kind)` / `Prepared::Fresh` branch, reached when the volume side is blank but a retired backup exists at `<target>.pre-ext4`.
## Required outcome
- Emit a warn-level log when starting a fresh entry while a sibling `*.pre-ext4` backup exists, so the paired-image invariant violation is diagnosable.
## Suggested approach (optional)
- In the `Fresh` branch (or in the caller `metadata_volume.rs`), check for `path_with_suffix(target, RETIRED_SUFFIX)` existence and `tracing::warn!` when present.Claude Opus | 𝕏
Greptile SummaryThis PR moves fsync-heavy container metadata to a dedicated ext4 volume. The main changes are:
Confidence Score: 5/5This looks safe to merge. The copied files and every copied directory are synced before publication. The published volume entry is synced before the original source is retired. No blocking issues remain in the updated migration path.
What T-Rex did
Important Files Changed
Reviews (2): Last reviewed commit: "fix(agent): make migration publish durab..." | Re-trigger Greptile |
Review findings on #497: the publish rename (ext4 volume) and the retire rename (btrfs source) live on different filesystems, so the volume-root sync must be mandatory or a crash could persist the retire while losing the publish — next boot would start empty. Also fsync each copied directory (per-file fsync does not contractually persist dentries) and document the metadata images in the daemon-lifecycle cleanup contract.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the incremental delta since the prior pullfrog review (101ac9bc): a crash-safety durability fix in the migration state machine, plus documentation rewording.
- Migration publish now durable before retire —
copy_dir_syncedfsyncs each directory after populating it, andprepare_entryfsyncsvolume_rootafter the publishrename(before the btrfs-side retire), with both syncs promoted from best-effort to error-propagating (?). This directly addresses the prior review's directory-entry durability gap. docs/daemon-lifecycle.md— rewordeddocker.imgreferences to "disk images" (plural) to cover the newdocker-meta.imgand Rosetta counterparts.
Traced the durability fix: the publish sync targets the ext4 volume and is issued synchronously before the btrfs retire rename, so no cross-filesystem crash window can persist the retire while losing the publish — the state machine re-converges either way. The prior three inline threads (config.rs rosetta accessor, migration dentry durability, VZ .fsync blast radius) are all resolved with author replies.
Claude Opus | 𝕏

Summary
Implements
internal-docs/plans/ext4-metadata-volume.md(committed in this PR, incl. measured results in §9): a 2 GiB sparsedocker-meta.imgattached as vdc, formatted guest-side as journaled ext4 (+fast_commit), carrying exactly the five profiled fsync-hot boltdb locations, bind-mounted over their btrfs paths before containerd/dockerd start. Bulk data (layers, blobs, volumes) stays on compressed btrfs.Implementation surfaced a third root cause: the VZ disk attachment defaulted to
synchronizationMode = .full— every guest FLUSH cost ~10 ms of host F_FULLFSYNC. A dedicated commit switches it to.fsync, aligning VZ with the custom-HV backend (whose block worker has always used plain fsync) and with the durability level Colima/OrbStack ship.Measured (same host, same day)
Guest
dd conv=fsync(avg of 20): VZ.full10.3 ms → VZ.fsync1.7 ms on the ext4 volume (HV reference 2.2 ms, Colima 1.0 ms).docker create(snapshot-prepare was ~182 ms alone)docker run --rm alpine trueRemaining
docker startgap (~330 ms) is in the network-endpoint/iptables portion — outside both ABX-496 root causes, to be filed separately.Upgrade / downgrade
block_devicesdrift arm (recreation rewrites the machine record only; image files untouched).arcbox.docker_metadata_device=/dev/vdcon the cmdline: declared-but-missing hard-fails; undeclared (older daemon) skips with zero probe delay..partial+ atomic rename, then retire the btrfs source to*.pre-ext4(prevents a recreated blank volume from resurrecting stale state; doubles as the manual downgrade artifact). Every crash window is unit-tested.Validation (all green)
cargo test -p arcbox-agent --lib metadata_migrate: 8/8 crash-window testscargo test -p arcbox-core vm_lifecycle: 53/53 incl. new drift + filename testsassets.lockbumped hereboot_assetse2e full path (format → bind → full Docker lifecycle): VZ 122 s ok, HV 170 s ok