fix(threads): write messages.jsonl atomically to prevent corruption (#8019)#8071
fix(threads): write messages.jsonl atomically to prevent corruption (#8019)#8071greatjourney589 wants to merge 1 commit into
Conversation
Review — PR #8071: fix(threads): write messages.jsonl atomically to prevent corruptionSummaryReplaces the direct Key FindingsCorrectness — Good
Tests — Thorough
Minor Observations
Recommendationcan merge — Well-implemented fix with good test coverage for a real data-loss scenario. |
Code ReviewSummary: This PR fixes a data corruption risk in Findings:
Recommendation: Can merge. This is a solid fix for a real data corruption risk, with good test coverage. |
|
Have you tested this on Windows? |
Yes, I did. Could you approve workflows and then merge this PR? @qnixsynapse |
Review -- PR #8071: fix(threads): write messages.jsonl atomically to prevent corruptionSummary: Replaces direct Observations:
Recommendation: can merge |
Summary
modify_messageanddelete_messagerewrote a thread'smessages.jsonlviawrite_messages_to_file, which opened the target withFile::create- POSIXO_TRUNC | O_WRONLY. The file was truncated before the new contents were written, so if the process was interrupted mid-write (force-close, SIGKILL, power loss, OOM),messages.jsonlwas left empty or partially written and the thread failed to load on the next launch.Per-thread async locks already serialize writes, but locks don't help against process-level interruption.
Changes
src-tauri/src/core/threads/helpers.rs-write_messages_to_filenow writes to a siblingmessages.jsonl.tmp, flushes + best-effortsync_all, then atomically renames it over the target. On any failure the tmp file is cleaned up, so an interrupted write leaves the previousmessages.jsonlfully intact.fs::renameis atomic on POSIX (same filesystem) and maps toMoveFileExWwithMOVEFILE_REPLACE_EXISTINGon Windows, so the destination is never observably missing or partial.src-tauri/src/core/threads/tests.rs- 3 new regression tests.Call sites in
commands.rs(modify_message,delete_message) are unchanged - the atomicity is entirely inside the helper.Test plan
cargo test --no-default-features --features test-tauri --lib core::threads::- all 35 thread tests pass, including the 3 new ones:test_write_messages_leaves_no_tmp_file_on_successtest_write_messages_overwrites_stale_tmp_from_prior_crashtest_write_messages_preserves_existing_on_write_failuretest_modify_and_delete_message,test_write_messages_overwrites_existing,test_concurrent_message_operationsstill pass (no regression on the happy path).Before / after
Backend-only fix - no UI. Demonstrated via tests rather than screenshots.
Before:
File::create(path)truncates the messages file, then writes message-by-message. A crash in between leaves a corruptmessages.jsonland the new regression tests fail againstmain.After: Writes land in
messages.jsonl.tmp, then a single atomic rename replaces the target. A mid-write crash leaves the previous file intact; a successful write leaves no.tmpresidue. A stale.tmpfrom a prior crashed run is harmlessly overwritten by the next write.Related
Fixes #8019.