aboutsummaryrefslogtreecommitdiff
path: root/internal/persistence/txn_directory_durability_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-20 15:35:58 -0400
committerhistoria <[not public]>2026-09-20 15:35:58 -0400
commit91b466b6923d3eb1df1658199960fe7aa6db36dc (patch)
treee8a5f308530037f9471e19e6f194177c784c36b6 /internal/persistence/txn_directory_durability_test.go
parentbab5dd653039d026eb3d4e46da7aafef7137fe7c (diff)
downloadthehouseoficarus-91b466b6923d3eb1df1658199960fe7aa6db36dc.tar.gz
feat: new placeholder world
Diffstat (limited to 'internal/persistence/txn_directory_durability_test.go')
-rw-r--r--internal/persistence/txn_directory_durability_test.go175
1 files changed, 175 insertions, 0 deletions
diff --git a/internal/persistence/txn_directory_durability_test.go b/internal/persistence/txn_directory_durability_test.go
new file mode 100644
index 0000000..134580e
--- /dev/null
+++ b/internal/persistence/txn_directory_durability_test.go
@@ -0,0 +1,175 @@
+package persistence
+
+import (
+ "errors"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestTransactionSyncsChangedDirectoriesBeforeCommit(t *testing.T) {
+ for _, operation := range []string{"delete", "move same directory", "move across directories"} {
+ t.Run(operation, func(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ srcDir := filepath.Join(dir, "source")
+ dstDir := srcDir
+ if operation == "move across directories" {
+ dstDir = filepath.Join(dir, "destination")
+ }
+ src, dst := filepath.Join(srcDir, "a.yaml"), filepath.Join(dstDir, "b.yaml")
+ writeTestFile(t, src, "source")
+ writeTestFile(t, dst, "destination")
+ txn := NewTransaction(journalDir)
+ var err error
+ if operation == "delete" {
+ err = txn.Delete(src)
+ } else {
+ err = txn.Move(src, dst)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ var synced []string
+ txn.dirSync = func(d string) error {
+ entries, err := ListJournalFiles(journalDir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d != journalDir && len(entries) != 1 {
+ t.Fatal("journal retired before data directory sync")
+ }
+ if d == journalDir && len(entries) != 0 {
+ t.Fatal("commit point did not remove journal")
+ }
+ if _, err := os.Stat(src); !os.IsNotExist(err) {
+ t.Fatalf("sync must follow deletion/rename: %v", err)
+ }
+ synced = append(synced, d)
+ return syncDir(d)
+ }
+ if err := txn.Commit(); err != nil {
+ t.Fatal(err)
+ }
+ want := []string{srcDir}
+ if dstDir != srcDir {
+ want = append(want, dstDir)
+ }
+ want = append(want, journalDir)
+ if !reflect.DeepEqual(synced, want) {
+ t.Fatalf("sync order = %v, want %v", synced, want)
+ }
+ if operation != "delete" && readTestFile(t, dst) != "source" {
+ t.Fatal("move did not replace destination")
+ }
+ })
+ }
+}
+
+func TestTransactionDataSyncFailureRollsBack(t *testing.T) {
+ for _, failAt := range []string{"delete", "move source", "move destination"} {
+ t.Run(failAt, func(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ srcDir, dstDir := filepath.Join(dir, "source"), filepath.Join(dir, "destination")
+ src, dst := filepath.Join(srcDir, "a.yaml"), filepath.Join(dstDir, "b.yaml")
+ writeTestFile(t, src, "source")
+ writeTestFile(t, dst, "destination")
+ txn := NewTransaction(journalDir)
+ var err error
+ if failAt == "delete" {
+ err = txn.Delete(src)
+ } else {
+ err = txn.Move(src, dst)
+ }
+ if err != nil {
+ t.Fatal(err)
+ }
+ failed := false
+ fault := errors.New("injected data directory sync failure")
+ txn.dirSync = func(d string) error {
+ failDir := srcDir
+ if failAt == "move destination" {
+ failDir = dstDir
+ }
+ if d == failDir {
+ failed = true
+ return fault
+ }
+ // Restoring existing before-images uses WriteFile, which
+ // already syncs their directories before journal removal.
+ if d == journalDir && (readTestFile(t, src) != "source" || readTestFile(t, dst) != "destination") {
+ t.Fatal("journal retired before rollback restored files")
+ }
+ return syncDir(d)
+ }
+ if err := txn.Commit(); !errors.Is(err, fault) || !failed {
+ t.Fatalf("commit error = %v, fault injected = %t", err, failed)
+ }
+ entries, err := ListJournalFiles(journalDir)
+ if err != nil || len(entries) != 0 {
+ t.Fatalf("successful rollback should retire journal: %v, %v", entries, err)
+ }
+ })
+ }
+}
+
+func TestRollbackDeletionSyncFailureRetainsJournalAndRetries(t *testing.T) {
+ dir := t.TempDir()
+ journalDir := filepath.Join(dir, ".journal")
+ createdDir, failDir := filepath.Join(dir, "created"), filepath.Join(dir, "fail")
+ if err := os.Mkdir(createdDir, 0700); err != nil {
+ t.Fatal(err)
+ }
+ created, deleted := filepath.Join(createdDir, "new.yaml"), filepath.Join(failDir, "old.yaml")
+ writeTestFile(t, deleted, "before")
+ txn := NewTransaction(journalDir)
+ if err := txn.Replace(created, []byte("new"), 0600); err != nil {
+ t.Fatal(err)
+ }
+ if err := txn.Delete(deleted); err != nil {
+ t.Fatal(err)
+ }
+ var synced []string
+ txn.dirSync = func(d string) error {
+ synced = append(synced, d)
+ return errors.New("injected directory sync failure")
+ }
+ if err := txn.Commit(); err == nil || !strings.Contains(err.Error(), "rollback failed") {
+ t.Fatalf("expected apply and rollback sync failures, got %v", err)
+ }
+ if !reflect.DeepEqual(synced, []string{failDir, createdDir}) {
+ t.Fatalf("syncs = %v; missing rollback deletion barrier", synced)
+ }
+ if _, err := os.Stat(created); !os.IsNotExist(err) {
+ t.Fatalf("rollback should have removed created file: %v", err)
+ }
+ if entries, err := ListJournalFiles(journalDir); err != nil || len(entries) != 1 {
+ t.Fatalf("failed rollback must retain journal: %v, %v", entries, err)
+ }
+
+ // Retry the same rollback with the file already absent: skipping its
+ // directory sync would incorrectly retire an undurable deletion.
+ synced = nil
+ txn.dirSync = func(d string) error {
+ synced = append(synced, d)
+ return syncDir(d)
+ }
+ if err := txn.rollback(); err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(synced, []string{createdDir}) {
+ t.Fatalf("retry syncs = %v, want %s", synced, createdDir)
+ }
+ if err := Recover(journalDir); err != nil {
+ t.Fatal(err)
+ }
+ if readTestFile(t, deleted) != "before" {
+ t.Fatal("recovery did not restore deleted file")
+ }
+ if entries, err := ListJournalFiles(journalDir); err != nil || len(entries) != 0 {
+ t.Fatalf("recovery did not retire journal: %v, %v", entries, err)
+ }
+}