From c054b27f628f23fecc505b9e09e78721738f3c4e Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 16 Sep 2026 13:52:58 -0400 Subject: fix: shutdown admission, recovery identity/order, rollback journal retention, pickup merge --- internal/persistence/txn.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'internal/persistence/txn.go') diff --git a/internal/persistence/txn.go b/internal/persistence/txn.go index 7d86cc8..93d186a 100644 --- a/internal/persistence/txn.go +++ b/internal/persistence/txn.go @@ -11,6 +11,11 @@ package persistence // Transactions do NOT serialize concurrent callers: like single-file saves, // they must be executed by an owner that prevents concurrent mutations of // the same files (the game's owner goroutine). +// +// On apply failure the applied operations are rolled back and the journal is +// removed. If the ROLLBACK itself fails, the journal is deliberately kept: +// its before-images are then the only durable record of the pre-transaction +// state, and the next Recover retries the rollback. import ( "crypto/rand" @@ -184,7 +189,13 @@ func (t *Transaction) Commit() error { if err := t.apply(); err != nil { if rbErr := t.rollback(); rbErr != nil { - err = fmt.Errorf("%v; rollback failed: %v", err, rbErr) + // The rollback could not restore the before-images: the journal + // is now the only durable record of the pre-transaction state. + // KEEP it so the next Recover call (or operator repair of the + // underlying fault) can retry the rollback instead of silently + // losing the before-images forever. See + // TestCommitKeepsJournalAfterFailedRollback. + return fmt.Errorf("%v; rollback failed (journal kept for recovery: %s): %v", err, journalPath, rbErr) } if rErr := os.Remove(journalPath); rErr != nil && !os.IsNotExist(rErr) { err = fmt.Errorf("%v; journal cleanup failed: %v", err, rErr) -- cgit v1.2.3