Skip to content
This repository was archived by the owner on May 13, 2026. It is now read-only.

chore(persistence): atomic-write AppleSyncConfig::save - #913

Merged
shiba4life merged 1 commit into
mainfrom
chore/atomic-write-config-saves
May 6, 2026
Merged

chore(persistence): atomic-write AppleSyncConfig::save#913
shiba4life merged 1 commit into
mainfrom
chore/atomic-write-config-saves

Conversation

@shiba4life

@shiba4life shiba4life commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR #868 introduced write_atomic_0600 (tmpfile + fsync + rename); a follow-up landed the helper as crate::utils::fs_atomic::write_atomic with an explicit mode: Option<u32> and converted three of the four remaining daemon-runtime config saves (save_node_config, IngestionConfig::save_to_file, IngestionConfig::write_saved_to_disk).

AppleSyncConfig::save was the only production save site still using plain std::fs::write. A power loss or OOM-kill mid-write would leave a half-written / zero-byte JSON file, and load() silently returns Default::default() — every successful or failed Apple auto-sync passes through this path, so the user's auto-sync schedule + last-error state could vanish without an error.

This PR routes it through fs_atomic::write_atomic(path, data, None) to match the pattern the other three plaintext writers use. The helper's docs (src/utils/fs_atomic.rs:11) explicitly say "plaintext config writers pass mode = None", and the write_atomic_without_mode_honors_umask test there guards against the regression of using 0o600 on plaintext.

Sites converted

  • src/ingestion/apple_import/sync_config.rs:176AppleSyncConfig::save (every Apple auto-sync attempt, success or failure)

Out of scope (deferred to follow-up)

  • src/handlers/auth.rs:1101write_bootstrap_status (one-shot during onboarding)
  • src/bin/folddb/restore.rs:160,194 — identity-restore writes
  • src/bin/folddb/commands/setup.rs — fresh-setup write
  • src/server/routes/file_upload.rs — multipart upload tempfile (different concern)

Tests

Two new regression tests in ingestion::apple_import::sync_config::tests:

  • save_uses_atomic_write_no_stale_tmpfile — after save() succeeds, no <path>.tmp sibling remains and the file parses cleanly via load().
  • save_recovers_from_stale_tmpfile — a leftover <path>.tmp from a prior crash does not block the next save(); the tmpfile is opened with truncate(true), so stale staged bytes are silently overwritten.

Both serialize on a NODE_CONFIG_LOCK mutex since they mutate the process-wide NODE_CONFIG env var to redirect config_path() at a tempdir.

Test plan

  • cargo clippy --workspace --all-targets -- -D warnings (clean apart from a pre-existing apple_import.rs macOS-only dead-code warning that does not fire in Linux CI)
  • cargo check --workspace
  • cargo test --workspace --lib -- --test-threads=1 — 980 passed
  • Lint scripts: redaction, spawn-instrument, tracing-egress, rev-pin-format, no-hardcoded-urls all pass

🤖 Generated with Claude Code

@shiba4life
shiba4life enabled auto-merge May 6, 2026 12:24
PR #868 introduced `write_atomic_0600` (tmpfile + fsync + rename); a
follow-up landed the helper as `crate::utils::fs_atomic::write_atomic`
with an explicit `mode: Option<u32>` and converted three of the four
remaining daemon-runtime config saves (`save_node_config`,
`IngestionConfig::save_to_file`, `IngestionConfig::write_saved_to_disk`).

`AppleSyncConfig::save` was the only production save site still using
plain `std::fs::write`. A power loss or OOM-kill mid-write would leave a
half-written / zero-byte JSON file, and `load()` silently returns
`Default::default()` — every successful or failed Apple auto-sync passes
through this path, so the user's auto-sync schedule + last-error state
could vanish without an error.

Route it through `fs_atomic::write_atomic(path, data, None)` to match
the pattern the other three plaintext writers use (umask applies; the
helper's docs explicitly say "plaintext config writers pass `mode = None`").

Test: two new regression tests in `apple_import::sync_config::tests`
assert that `save()` leaves no stale `<path>.tmp` sibling on success,
and that a leftover tmpfile from a prior crash is silently overwritten
on retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shiba4life
shiba4life force-pushed the chore/atomic-write-config-saves branch from bb547fa to fc7c6b7 Compare May 6, 2026 12:28
@shiba4life shiba4life changed the title chore(persistence): atomic-write all daemon JSON config saves chore(persistence): atomic-write AppleSyncConfig::save May 6, 2026
@shiba4life
shiba4life disabled auto-merge May 6, 2026 12:29
@shiba4life
shiba4life enabled auto-merge May 6, 2026 12:29
@shiba4life
shiba4life added this pull request to the merge queue May 6, 2026
Merged via the queue into main with commit 5df5cb2 May 6, 2026
11 checks passed
@shiba4life
shiba4life deleted the chore/atomic-write-config-saves branch May 6, 2026 12:39
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant