aboutsummaryrefslogtreecommitdiff
path: root/internal/persistence/txn.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.go
parentbab5dd653039d026eb3d4e46da7aafef7137fe7c (diff)
downloadthehouseoficarus-91b466b6923d3eb1df1658199960fe7aa6db36dc.tar.gz
feat: new placeholder world
Diffstat (limited to 'internal/persistence/txn.go')
-rw-r--r--internal/persistence/txn.go60
1 files changed, 41 insertions, 19 deletions
diff --git a/internal/persistence/txn.go b/internal/persistence/txn.go
index 9939204..400d355 100644
--- a/internal/persistence/txn.go
+++ b/internal/persistence/txn.go
@@ -64,9 +64,9 @@ type Transaction struct {
ops map[string]*fileOp
order []string
journalDir string
- // dirSync makes a journal directory entry durable (nil means syncDir).
- // It is a field rather than a package-level function so a test can
- // assert that the commit point is synced and inject a sync failure.
+ // dirSync makes directory mutations durable (nil means syncDir).
+ // Per-transaction so tests can check ordering and inject failures
+ // without changing a package-global hook.
dirSync func(string) error
}
@@ -82,12 +82,31 @@ func syncDir(dir string) error {
return d.Sync()
}
-// syncJournalDir syncs the transaction's journal directory.
-func (t *Transaction) syncJournalDir() error {
+func (t *Transaction) syncDirectory(dir string) error {
if t.dirSync != nil {
- return t.dirSync(t.journalDir)
+ return t.dirSync(dir)
}
- return syncDir(t.journalDir)
+ return syncDir(dir)
+}
+
+// removeFileDurable also syncs an already-absent file's parent: recovery may
+// be retrying a deletion whose previous directory sync failed. A missing
+// parent needs no sync because this transaction does not create directories.
+func (t *Transaction) removeFileDurable(path string) error {
+ err := os.Remove(path)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ dir := filepath.Dir(path)
+ if os.IsNotExist(err) {
+ if _, statErr := os.Stat(dir); os.IsNotExist(statErr) {
+ return nil
+ }
+ }
+ if err := t.syncDirectory(dir); err != nil {
+ return fmt.Errorf("sync directory %s after delete: %w", dir, err)
+ }
+ return nil
}
// removeJournalDurable unlinks a journal and syncs its directory, so the
@@ -101,7 +120,7 @@ func (t *Transaction) removeJournalDurable(path string) error {
for attempt := 0; attempt < journalRemoveAttempts; attempt++ {
removeErr = os.Remove(path)
if removeErr == nil || os.IsNotExist(removeErr) {
- if err := t.syncJournalDir(); err != nil {
+ if err := t.syncDirectory(t.journalDir); err != nil {
return fmt.Errorf("journal %s removed but its directory could not be synced (a crash may resurrect the journal and roll the transaction back): %w", path, err)
}
return nil
@@ -291,7 +310,7 @@ func (t *Transaction) apply() error {
return fmt.Errorf("write %s: %w", op.Path, err)
}
case kindDelete:
- if err := os.Remove(op.Path); err != nil && !os.IsNotExist(err) {
+ if err := t.removeFileDurable(op.Path); err != nil {
return fmt.Errorf("delete %s: %w", op.Path, err)
}
case kindMove:
@@ -306,6 +325,17 @@ func (t *Transaction) apply() error {
if err := os.Rename(op.Path, op.MoveTo); err != nil {
return fmt.Errorf("move %s: %w", op.Path, err)
}
+ // A cross-directory rename changes both directory entries. Make
+ // both durable before the recovery journal can be retired.
+ srcDir, dstDir := filepath.Dir(op.Path), filepath.Dir(op.MoveTo)
+ if err := t.syncDirectory(srcDir); err != nil {
+ return fmt.Errorf("sync move source directory %s: %w", srcDir, err)
+ }
+ if dstDir != srcDir {
+ if err := t.syncDirectory(dstDir); err != nil {
+ return fmt.Errorf("sync move destination directory %s: %w", dstDir, err)
+ }
+ }
case kindMoveTarget:
// Content replace after the move delivered the source bytes.
if op.NewExists {
@@ -333,21 +363,13 @@ func (t *Transaction) rollback() error {
if op.OldExists {
return WriteFile(path, op.Old, os.FileMode(op.OldMode))
}
- return os.Remove(path)
+ return t.removeFileDurable(path)
}
switch op.Kind {
- case kindMove, kindMoveTarget:
- if err := restore(op.Path); err != nil && !os.IsNotExist(err) {
- record(fmt.Errorf("restore %s: %w", op.Path, err))
- }
- case kindReplace:
+ case kindMove, kindMoveTarget, kindReplace, kindDelete:
if err := restore(op.Path); err != nil {
record(fmt.Errorf("restore %s: %w", op.Path, err))
}
- case kindDelete:
- if err := restore(op.Path); err != nil && !os.IsNotExist(err) {
- record(fmt.Errorf("restore %s: %w", op.Path, err))
- }
}
}
return firstErr