fix(core): wait for in-flight save before shutting down systems - #5385
fix(core): wait for in-flight save before shutting down systems#5385soloturn wants to merge 1 commit into
Conversation
#704: events getting sent to ComponentSystems after their shutdown() has already run, causing NPEs from internal state the system's own shutdown() had already cleared (original report: SignalSystem. producerRemoved crashing on a null map during host shutdown). ## Root cause StateIngame.dispose() calls storageManager. waitForCompletionOfPreviousSaveAndStartSaving(), which starts a SaveTransaction on GameScheduler.parallel() - a genuine background thread, per ReadWriteStorageManager.startSaving()'s Mono.fromRunnable(...).subscribeOn(GameScheduler.parallel()). subscribe(). That transaction's run() calls privateEntityManager. deactivateForStorage(entityRef) while serializing entities, which sends deactivation events to whatever ComponentSystems are still registered to handle them. dispose() went on to call componentSystemManager.shutdown() while that save was still running in the background, and only blocked on it much later via storageManager.finishSavingAndShutdown() - after componentSystemManager.shutdown(), PhysicsEngine.dispose(), and entityManager.clear() had already run. Any deactivation event the save dispatched in that window landed on a system whose own shutdown() may have already nulled out the state its event handler needed - the exact NPE in #704's stack trace, just relocated from the 2013-era synchronous save path to the current async one. ## Fix Move the finishSavingAndShutdown() blocking call to immediately before componentSystemManager.shutdown(), so the save (and every event it dispatches) is guaranteed complete before any system, physics, or entity teardown begins. No new logic - two existing calls reordered. ## Verification :engine:compileJava clean. Ran the full engine-tests:integrationTest suite (18 MTE test classes, 34 tests) - StateIngame.dispose() is exactly the method every test's teardown goes through, so this is a meaningful regression check for the reordering. All 34 pass. This is inherently hard to verify beyond that: reproducing #704 needs an actual multiplayer host mid-save at the moment of shutdown, which isn't something I can drive here. The integration suite exercising this exact dispose() path on every test teardown, 34/34 green, is the best available signal that the reorder doesn't break normal shutdown. Fixes #704 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review. 📝 WalkthroughSummary by CodeRabbit
Walkthrough
ChangesIn-game shutdown
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The shutdown ordering now waits for in-flight saves before tearing down systems, preventing late save events from reaching cleared state. The change is localized and verified by the reported checks; no actionable merge-blocking risk remains after normal review. Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Fixes #704 - events getting sent to
ComponentSystems after theirshutdown()has already run, causing NPEs from internal state the system's ownshutdown()had already cleared (original report:SignalSystem.producerRemovedcrashing on a null map during host shutdown).Root cause
StateIngame.dispose()callsstorageManager.waitForCompletionOfPreviousSaveAndStartSaving(), which starts aSaveTransactiononGameScheduler.parallel()- a genuine background thread, perReadWriteStorageManager.startSaving()'sMono.fromRunnable(...).subscribeOn(GameScheduler.parallel()).subscribe(). That transaction'srun()callsprivateEntityManager.deactivateForStorage(entityRef)while serializing entities, which sends deactivation events to whateverComponentSystems are still registered to handle them.dispose()went on to callcomponentSystemManager.shutdown()while that save was still running in the background, and only blocked on it much later viastorageManager.finishSavingAndShutdown()- aftercomponentSystemManager.shutdown(),PhysicsEngine.dispose(), andentityManager.clear()had already run. Any deactivation event the save dispatched in that window landed on a system whose ownshutdown()may have already nulled out the state its event handler needed - the exact NPE in #704's stack trace, just relocated from the 2013-era synchronous save path to the current async one.Fix
Move the
finishSavingAndShutdown()blocking call to immediately beforecomponentSystemManager.shutdown(), so the save (and every event it dispatches) is guaranteed complete before any system, physics, or entity teardown begins. No new logic - two existing calls reordered.Verification
:engine:compileJavaclean. Ran the fullengine-tests:integrationTestsuite (18 MTE test classes, 34 tests) -StateIngame.dispose()is exactly the method every test's teardown goes through, so this is a meaningful regression check for the reordering. All 34 pass.This is inherently hard to verify beyond that: reproducing #704 needs an actual multiplayer host mid-save at the moment of shutdown, which isn't something I can drive here. The integration suite exercising this exact
dispose()path on every test teardown, 34/34 green, is the best available signal that the reorder doesn't break normal shutdown.