diff options
| author | historia <[not public]> | 2026-09-20 15:35:58 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-20 15:35:58 -0400 |
| commit | 91b466b6923d3eb1df1658199960fe7aa6db36dc (patch) | |
| tree | e8a5f308530037f9471e19e6f194177c784c36b6 /internal/persistence/txn.go | |
| parent | bab5dd653039d026eb3d4e46da7aafef7137fe7c (diff) | |
| download | thehouseoficarus-91b466b6923d3eb1df1658199960fe7aa6db36dc.tar.gz | |
feat: new placeholder world
Diffstat (limited to 'internal/persistence/txn.go')
| -rw-r--r-- | internal/persistence/txn.go | 60 |
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 |
