Files
Tobias GesellchenandClaude Opus 4.8 b1a5428ebf fix(datastore): fsync atomicWriteFile for crash-safe durability (#458)
atomicWriteFile wrote a temp file and renamed it, but never fsync'd — so an
unclean power-cut on a journaling NAND filesystem (UBIFS on the speaker's
/mnt/nv) could leave the renamed datastore file present but 0 bytes (the rename
was journalled, the data blocks were not flushed). Now fsync the temp file
before the rename and the parent directory after, via os.Root.OpenFile/Open;
directory fsync is best-effort (unsupported on some filesystems).

Pairs with the read-side resilience fix (#459): durability prevents the 0-byte
files; resilience tolerates any that already exist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:38:12 +02:00

53 lines
1.5 KiB
Go

package datastore
import (
"os"
"path/filepath"
"testing"
)
// TestAtomicWriteFile_DurableRoundTrip guards the durable write path added for
// #458: content must round-trip, an overwrite must truncate cleanly, and no
// `.tmp` sidecar may be left behind. (The fsync durability itself isn't
// unit-testable without power-loss fault injection; this is the functional
// regression guard so the fsync rework doesn't break writes.)
func TestAtomicWriteFile_DurableRoundTrip(t *testing.T) {
tempDir, err := os.MkdirTemp("", "st-atomic-test-*")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(tempDir) })
ds := NewDataStore(tempDir)
dir := ds.AccountDeviceDir("1234567", "001122334455")
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, "Sources.xml")
if err := ds.atomicWriteFile(path, []byte("first")); err != nil {
t.Fatalf("atomicWriteFile (create) failed: %v", err)
}
if got, _ := os.ReadFile(path); string(got) != "first" {
t.Errorf("content after create = %q, want %q", got, "first")
}
// Overwrite must truncate the previous (longer) content, not leave a tail.
if err := ds.atomicWriteFile(path, []byte("hi")); err != nil {
t.Fatalf("atomicWriteFile (overwrite) failed: %v", err)
}
if got, _ := os.ReadFile(path); string(got) != "hi" {
t.Errorf("content after overwrite = %q, want %q", got, "hi")
}
// No leftover temp sidecar.
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
t.Errorf("expected no %s.tmp leftover, stat err = %v", path, err)
}
}