diff options
Diffstat (limited to 'internal/persistence/txn_rollback_test.go')
| -rw-r--r-- | internal/persistence/txn_rollback_test.go | 102 |
1 files changed, 102 insertions, 0 deletions
diff --git a/internal/persistence/txn_rollback_test.go b/internal/persistence/txn_rollback_test.go new file mode 100644 index 0000000..2bd7974 --- /dev/null +++ b/internal/persistence/txn_rollback_test.go @@ -0,0 +1,102 @@ +package persistence + +// Regression: when a transaction's apply fails AND its rollback fails, the +// journal is the only durable record of the pre-transaction before-images. +// Deleting it at commit time made the corruption permanent; it must be kept +// so the next Recover call (or operator repair) can retry the rollback. + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCommitKeepsJournalAfterFailedRollback(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "character.yaml") + if err := os.WriteFile(path, []byte("before"), 0600); err != nil { + t.Fatal(err) + } + journalDir := filepath.Join(dir, "journals") + txn := NewTransaction(journalDir) + if err := txn.Replace(path, []byte("after"), 0600); err != nil { + t.Fatal(err) + } + // Deterministically force both the apply and the rollback writes to + // fail after the snapshot: the target becomes a nonempty directory, + // so WriteFile cannot create the file and rollback cannot restore it. + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(path, 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(path, "blocker"), []byte("x"), 0600); err != nil { + t.Fatal(err) + } + + err := txn.Commit() + if err == nil || !strings.Contains(err.Error(), "rollback failed") { + t.Fatalf("need an apply+rollback failure, got: %v", err) + } + entries, err := ListJournalFiles(journalDir) + if err != nil { + t.Fatal(err) + } + if len(entries) == 0 { + t.Fatal("failed rollback deleted the only durable before-image; restart cannot recover the pre-transaction state") + } +} + +// TestRecoverRetriesKeptRollbackJournal: after the operator repairs the +// fault that broke the rollback, the next Recover completes it. +func TestRecoverRetriesKeptRollbackJournal(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "character.yaml") + if err := os.WriteFile(path, []byte("before"), 0600); err != nil { + t.Fatal(err) + } + journalDir := filepath.Join(dir, "journals") + txn := NewTransaction(journalDir) + if err := txn.Replace(path, []byte("after"), 0600); err != nil { + t.Fatal(err) + } + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + if err := os.Mkdir(path, 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(path, "blocker"), []byte("x"), 0600); err != nil { + t.Fatal(err) + } + if err := txn.Commit(); err == nil { + t.Fatal("expected the injected apply/rollback failure") + } + + // Operator repair: put the pre-transaction file back. + if err := os.RemoveAll(path); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("after"), 0600); err != nil { + t.Fatal(err) + } + if err := Recover(journalDir); err != nil { + t.Fatalf("recovery retry failed: %v", err) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(data) != "before" { + t.Fatalf("recovery did not roll back to the before-image: %q", data) + } + entries, err := ListJournalFiles(journalDir) + if err != nil { + t.Fatal(err) + } + if len(entries) != 0 { + t.Fatalf("completed recovery left journals behind: %v", entries) + } +} |
