Skip to content

Replace custom atomic state.json write with tempfile - #234

Draft
tupe12334 wants to merge 1 commit into
mainfrom
replace-custom-atomic-write-with-tempfile
Draft

tupe12334 wants to merge 1 commit into
mainfrom
replace-custom-atomic-write-with-tempfile

Conversation

@tupe12334

Copy link
Copy Markdown
Member

Opened by the moadim routine "Replace custom code with existing lib, or extract to new lib → draft PR / issue (one repo/run) → Slack". cc @tupe12334

The custom code today

core/src/state.rs lines 51–63 (save_state) hand-rolls an atomic write:

let dir = path.parent().unwrap_or_else(|| Path::new("."));
let tmp = dir.join(format!("state.json.tmp.{}", process::id()));
let content = serde_json::to_string_pretty(state)?;
fs::write(&tmp, content)?;
fs::rename(&tmp, path)?;

It writes state.json to a temp file named after the PID, then renames it over the target. Called from gate.rs:135 and gate.rs:159 on every gate event.

Gaps in the hand-rolled version:

  • Stale temp files. If fs::write or fs::rename fails, state.json.tmp.<pid> stays in the session dir forever.
  • Name collision. The temp name is unique per process, not per call. Two threads in the same process (e.g. an embedder of steplock-core, or parallel tests) that save the same session race on one temp file.

The proposed library

tempfile — NamedTempFile::new_in(dir) + persist(path).

  • The standard Rust crate for this (hundreds of millions of downloads, maintained under the Stebalien/tempfile repo, used by cargo itself).
  • License: MIT OR Apache-2.0 (compatible with this repo).
  • Already in the lockfile (3.27.0) as a dev-dependency. This PR only moves it to [dependencies]; Cargo.lock does not change.
  • Random per-call temp name in the same dir (so the rename stays atomic on the same filesystem), and the temp file is deleted on drop if anything fails before persist.

Before / after

// before
let tmp = dir.join(format!("state.json.tmp.{}", process::id()));
let content = serde_json::to_string_pretty(state)?;
fs::write(&tmp, content)?;
fs::rename(&tmp, path)?;

// after
let content = serde_json::to_string_pretty(state)?;
let mut tmp = tempfile::NamedTempFile::new_in(dir)?;
tmp.write_all(content.as_bytes())?;
tmp.persist(path).map_err(|e| e.error)?;

Call sites (gate.rs) are unchanged.

Trade-offs

  • Dependency weight: tempfile becomes a runtime dep of the steplock binary (pulls fastrand, rustix/windows-sys, once_cell, all already in the lockfile). Small.
  • File mode: NamedTempFile creates files as 0600, so state.json written by the binary becomes owner-only instead of umask default (0644). ack.sh and preview.sh run as the same user, so this does not break them. It is arguably better for session state.
  • No fsync — same as before. Add tmp.as_file().sync_all()? if crash durability matters later.

Verification

  • cargo test — all pass (19 + 11).
  • cargo clippy --all-targets — clean.

Checklist

  • Move tempfile from [dev-dependencies] to [dependencies]
  • Rewrite save_state with NamedTempFile::new_in + persist
  • Tests and clippy pass
  • Decide whether the 0600 mode on state.json is wanted (or chmod after persist)

🤖 Generated with Claude Code

Replace the hand-rolled PID-suffixed temp file + rename in save_state
with tempfile::NamedTempFile::persist. Temp file names are now unique
per call and the temp file is removed when a write fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tupe12334 tupe12334 added the enhancement New feature or request label Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant