Repository navigation
Conversation
The `experimental-file` remote cache provider built opendal's Fs service without `atomic_write_dir`, so opendal opened each blob's final path with O_TRUNC and wrote into it in place. An interrupted write (e.g. a killed Pants process) left a truncated blob under its digest's name. Reads detect the wrong digest, but never remove the blob, so the store stays broken until the file is deleted by hand. Set `atomic_write_dir` to a `.tmp` directory inside the store root, so each blob is written to a temporary file and renamed into place once complete. Keeping it under the root means the rename never crosses filesystems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PerformanceI measured the write cost with and without Per-blob time:
The "Without" column is from the opendal 0.58 runs; each percentage compares against the "Without" run on the same opendal version. On 0.53, the "Without" times were 5.34, 4.79, 5.00, 9.01 and 56.9 ms. The overhead is roughly 0.1–0.3 ms per blob (about 2–6%), and the run-to-run ranges overlap. The 0.53 1 MiB case even came out faster with temp files. Almost all of each write's cost is the same in both modes.
The rename itself is free. The extra cost appears only after a full flush, presumably because the rename then has to wait on the filesystem journal. That flush also limits this provider to about 200 blob writes per second regardless of concurrency. That's a separate, pre-existing cost that this PR neither adds nor removes. I haven't measured Linux, where There's no prior discussion of this setting on the Pants side. #19827 introduced the provider without it, and later changes to (Measured by Claude Code; the benchmark was a temporary |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Background
The
experimental-fileremote cache provider (remote_store_address = "file://...") builds opendal'sFsservice withoutatomic_write_dir. In that mode opendal opens each blob's final path withO_TRUNCand writes into it in place (viatokio::io::copy, in 8 KiB chunks). If the write is interrupted, for example because Pants is killed mid-upload, a truncated blob is left under its digest's name:list_missing_digestsreports the digest as present, so it is never re-uploaded.loadvalidates the digest and fails withRemote CAS gave wrong digest, but never removes the blob, so the store stays broken until the file is deleted by hand.abortis unsupported in this mode, so a failed upload can't be cleaned up either.We hit this in practice: every bad blob in our stores was truncated at an 8 KiB boundary.
Change
Provider::fssetsatomic_write_dirto a.tmpdirectory under the store root. opendal then writes each blob to a temporary file and renames it to its final path once complete, so readers see either the whole blob or nothing. The directory is under the root so the rename never crosses a filesystem boundary. opendal already calledsync_allon every write, so there is no extra fsync.If a process dies mid-write, its temporary file is left in
.tmp. That is harmless, but nothing currently cleans it up. Blobs that are already truncated are not repaired; the release note says to delete them.Tests
fs_partial_write_is_not_visible: starts a write, stops partway, and checks the blob is neither on disk under its final path nor loadable, and is still reported missing. It then finishes the write and checks the whole blob appears. This fails without the fix (assertion failed: !root.path().join(&path).exists()).fs_store_file_round_trip:store_fileagainst a real directory succeeds and leaves no temporary files behind.cargo test -p remote_provider_opendalpasses (19 tests).cargo clippyandcargo fmtare clean.LLM assistance notice
This PR was written primarily by Claude Code (Claude Opus 5.5). I directed the scope and reviewed the result.
🤖 Generated with Claude Code