Skip to content

Desktop atomic writes: fsync temp file + parent directory before/after rename for true crash durability #357

Description

@qnbs

TERMINAL R-15 RECONCILIATION — 2026-10-02

Closed through the canonical renderer-neutral Core implementation rather than a new standalone Tauri durability authority. #949 maps the original file-sync / atomic-promotion / directory-durability / reconciliation / fault-injection requirements to implementation and tests. The current legacy TS/Tauri path remains authoritative until the separately authorized Gate 7 cutover; packaged physical power-loss qualification remains Gate 6.

HISTORICAL / SUPERSEDED — CURRENT R-15 RECONCILIATION — 2026-10-01 23:12 CEST — superseded by terminal Gate 3 closure 2026-10-02

OWNER = Gate 3 / #921
GATE_3A_HEADLESS_DURABILITY = TERMINAL via #930
GATE_3B_COMMIT / STARTUP_RECONCILIATION = TERMINAL via #937 + #940
GATE_3C_PART_1_ROOT_DIGESTS = TERMINAL via #941 → main 03a24786f1c9fda76656dbbd2a635e324331ec81
GATE_3C_PART_2_CATALOG = ACTIVE via #942 @ 48525c99ed20c88774df161b8d8c0d474a785302
GATE_3C_PART_3 = REMAINING
ISSUE_STATE = OPEN
PACKAGED_POWER_LOSS_EVIDENCE = Gate 6 / #924 where source CI cannot prove it
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO

Renderer-neutral Rust Core now has durable staging/promotion, marker-based commit/startup reconciliation, and terminally proven authority-root digest primitives. #942 is building the authenticated record catalog. Do not close #357 yet: Gate 3 still needs the two-phase root/anchor commit, write-protocol integration, listing/retention, complete authority reconciliation (including the asset-pair boundary), and terminal Gate 3 evidence. Packaged physical/power-loss qualification remains Gate 6.

HISTORICAL / SUPERSEDED — CURRENT R-15 RECONCILIATION — 2026-10-01 — superseded 2026-10-01 23:12 CEST

OWNER = Gate 3 / #921
GATE_3A_HEADLESS_DURABILITY = TERMINAL via #930
GATE_3B_COMMIT / STARTUP_RECONCILIATION = TERMINAL via #937 + #940
CURRENT_MAIN = e43559b9bd6c5a8b6f5ba06ed9a2da42079551e1
CURRENT_3C = Part 1 active via #941 (authority-root digests)
REMAINING = 3C part 1 terminal → part 2 catalog/shards → part 3 two-phase root/anchor commit + Gate 3 closure evidence
ISSUE_STATE = OPEN
PACKAGED_POWER_LOSS_EVIDENCE = Gate 6 / #924 where source CI cannot prove it
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO

Renderer-neutral Rust Core now has durable staging/promotion plus marker-based commit/startup reconciliation on proven main. That materially advances this issue, but it still does not justify closure: Gate 3C must bind committed record state into the root/catalog authority and complete the terminal Gate 3 evidence. Keep this issue open through #941 and the remaining 3C parts. Do not substitute source-level fault injection for Gate 6 packaged power-loss qualification.

HISTORICAL / SUPERSEDED — Gate 3A-only reconciliation checkpoint — 2026-10-01

OWNER = Gate 3 / #921
GATE_3A_HEADLESS_DURABILITY = DELIVERED via #930
REMAINING = 3B commit-marker/startup reconciliation + 3C authority-root/catalog commit + Gate 3 closure evidence
PRODUCTION_AUTHORITY_SWITCH_ALLOWED = NO

The Tauri-only implementation sketch below is historical provenance, not the current architecture destination. #357 is now reconciled through renderer-neutral Rust Core Gate 3. Gate 3A already provides durable staging/promotion and platform sync semantics; do not close this issue until 3B/3C establish committed-authority/reconciliation semantics and Gate 3 records the final evidence. Packaged power-loss qualification remains Gate 6 where source/CI cannot prove it.


Raised by chatgpt-codex-connector during the #354 review correction loop (thread: #354 (comment) area — see PR #354 for full context).

The gap

services/fs/fsCore.ts's writeTextFileAtomic/writeFileAtomic (added in #354) write to a temp file then rename over the final path, closing the "torn/partial write" class of bug. But in the abrupt power-loss scenario specifically, this isn't a complete durability guarantee: awaiting @tauri-apps/plugin-fs's writeTextFile/writeFile only completes the equivalent of write_all() — it doesn't call fsync/sync_all(), and a rename is not itself a durability barrier on most filesystems. The OS can still hold the new content (and the directory-entry update from the rename) in a page cache buffer that hasn't hit stable storage. A reboot at exactly the wrong moment could in theory expose an empty, partial, or still-the-old-file state despite the JS-level await having already resolved.

Why not fixed in #354

The real fix needs a Rust-side Tauri command (JS-level plugin-fs doesn't expose fsync control): open the temp file, write, sync_all(), close, rename, then open and sync the parent directory too (POSIX best practice for a rename to be durable, not just atomic). This is meaningfully more surface area than a JS-only PR — new Rust command, capability wiring, and it can't be meaningfully tested in this environment (no practical way to build/run a packaged Tauri desktop app here to verify real fsync behavior under simulated power loss).

Scope for whoever picks this up

  • New Tauri command (e.g. write_file_durable) in src-tauri/src/commands/, wrapping write + sync_all() + rename + parent-dir sync.
  • Register in lib.rs, wire capability permissions.
  • Swap fsCore.ts's writeTextFileAtomic/writeFileAtomic to call it via invoke() instead of the plugin-fs JS API, on Tauri only (web build has no equivalent concept).
  • Needs rust-check (PR ci: add required Rust/Tauri compile gate on pull requests #353's new CI gate) to at least catch compile/lint issues; real durability behavior can only be verified on a packaged build under actual (or simulated, e.g. SIGKILL mid-write in a controlled test) power-loss conditions.

Lower priority than the torn-write fix already shipped in #354 — this hardens an already-real improvement further, it doesn't fix a regression.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions