Skip to content

fix(files,server): age-gate stale-artifact sweep to stop it deleting live concurrent exports#172

Merged
bug-ops merged 1 commit into
masterfrom
fix/169-stale-sweep-race
Jul 9, 2026
Merged

fix(files,server): age-gate stale-artifact sweep to stop it deleting live concurrent exports#172
bug-ops merged 1 commit into
masterfrom
fix/169-stale-sweep-race

Conversation

@bug-ops

@bug-ops bug-ops commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • FileSystem::sweep_stale_artifacts matched staging/displaced siblings by name alone, with no way to distinguish a genuine crash orphan from another live process's in-flight export. A concurrent export of the same target could delete the first export's staging directory outright, and if the timing landed between swap_into_place's two renames, the displaced original too — defeating the rollback and permanently losing the target with no recovery path.
  • The sweep now only removes a staging/stale sibling once it is at least STALE_ARTIFACT_MIN_AGE (5 minutes) old by mtime, reliably distinguishing a crash leftover from a live sibling's artifacts. swap_into_place's displaced backup has its mtime explicitly refreshed (via touch_dir on target, before the rename) so it's covered by the same age gate as staging, instead of inheriting the original target's already-stale mtime.
  • GeneratorService::save_categorized_tools gains a keyed Arc<Mutex<()>> per output_dir, mirroring the existing per-server-id introspectors lock, so two JSON-RPC calls targeting the same output directory no longer race on the underlying staging/swap.

Fixes #169

Test plan

  • cargo +nightly fmt --check
  • cargo +stable clippy --all-targets --all-features --workspace -- -D warnings
  • cargo nextest run --all-features --workspace --no-fail-fast (740 passed)
  • cargo test --doc --all-features --workspace
  • RUSTFLAGS="-D warnings" RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --no-deps --workspace --all-features
  • New regression tests: displace_existing_target_refreshes_displaced_mtime, sweep_stale_artifacts_skips_recent_orphans, sweep_stale_artifacts_older_than_removes_aged_orphans, test_export_does_not_sweep_recent_sibling_artifacts (mcp-files); test_export_lock_for_same_output_dir_shares_one_lock, test_export_lock_for_different_output_dirs_get_independent_locks, test_export_lock_stale_eviction_does_not_remove_unrelated_entry (mcp-server)

@github-actions github-actions Bot added area: bridge MCP bridge and integration crate: mcp-files Changes to mcp-files crate (virtual filesystem) type: documentation Documentation changes (*.md, docs/, comments) breaking change Contains breaking API changes (requires major version bump) release Release preparation (changelog, version bumps) labels Jul 9, 2026
…live concurrent exports

sweep_stale_artifacts previously matched staging/displaced siblings by
name alone, with no way to tell a genuine crash orphan from another
live process's in-flight export. A concurrent export of the same
target could delete the first export's staging directory outright,
and if the timing landed between swap_into_place's two renames, the
displaced original too, defeating the rollback and permanently losing
the target with no recovery path.

The sweep now only removes a staging/stale sibling once it is at
least STALE_ARTIFACT_MIN_AGE (5 minutes) old by mtime, which reliably
distinguishes a crash leftover from a live sibling's artifacts.
swap_into_place's displaced backup has its mtime explicitly refreshed
before the rename (a rename preserves the source's mtime rather than
resetting it), so it is covered by the same age gate as staging
instead of inheriting the original target's already-stale mtime.

GeneratorService::save_categorized_tools also gains a keyed
Arc<Mutex<()>> per output_dir, mirroring the existing per-server-id
introspectors lock, so two JSON-RPC calls targeting the same output
directory no longer race on the underlying staging/swap.

Fixes #169
@bug-ops
bug-ops force-pushed the fix/169-stale-sweep-race branch from b91d5bf to 857c5e5 Compare July 9, 2026 20:21
@bug-ops
bug-ops merged commit 31d840e into master Jul 9, 2026
17 checks passed
@bug-ops
bug-ops deleted the fix/169-stale-sweep-race branch July 9, 2026 20:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: bridge MCP bridge and integration breaking change Contains breaking API changes (requires major version bump) crate: mcp-files Changes to mcp-files crate (virtual filesystem) release Release preparation (changelog, version bumps) type: documentation Documentation changes (*.md, docs/, comments)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

files: stale-artifact sweep can delete a concurrent live export's staging dir, permanently losing the target directory

1 participant