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/game/game.go | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'internal/game/game.go') diff --git a/internal/game/game.go b/internal/game/game.go index 7f29d1f..ca38e27 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -135,6 +135,15 @@ func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig * // initialization cycle with the shutdown coordinator. g.shutdownHook = g.RequestShutdown g.Economy = newEconomyJournal(dataDir) + // Lifecycle-journal rollback MUST run before economy recovery: a rolled- + // back rename transaction restores an older character before-image, and + // if money compensation ran first its settled/compacted records would no + // longer be recoverable — the rollback would silently erase a payout + // that was already re-applied. Rollback also precedes every store read + // below (courses, mods, techs, craft index) that a leftover transaction + // may reference. See the TestEconomyRecoveryRunsAfterLifecycleRollback + // regression. + g.recoverJournals() // Compensate open money records from any prior crash before any // session can connect. g.recoverEconomy() @@ -144,9 +153,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, constantColorConfig * g.LoadTechs() g.buildCraftIndex() g.loadAllFlagTriggers() - // Journal recovery before any store reads files a leftover transaction - // may reference, then one-time stable-identity migration. - g.recoverJournals() + // One-time stable-identity migration. g.backfillCharacterIDs() g.ValidateAndLog() return g @@ -273,9 +280,22 @@ func (g *Game) saveAllOnlineCharacters() error { // HandleSessionOnOwner is the entry point handed to the network server: // connection goroutines submit one task per input line, so all game state -// mutation executes on the owner goroutine. +// mutation executes on the owner goroutine. Once shutdown has begun, new +// input is REJECTED (not queued and not executed inline): the shutdown +// sequence owns final persistence, and the transports keep draining until +// they close, so an accepted line could mutate characters after the final +// save or race the refund/save barrier. func (g *Game) HandleSessionOnOwner(sess *net.Session, input string) { + if g.isShuttingDown() { + return + } g.RunOnOwner(func() { + // Re-checked at task execution: the flag may flip between + // submission and execution, and RunOnOwner must never fall back + // to inline execution after the owner has stopped. + if g.isShuttingDown() { + return + } g.HandleSession(sess, input) }) } -- cgit v1.2.3