10 — Shutdown and flush lifecycle¶
Scope of this diagram¶
One-shot onDisable path for the Bukkit-family entry point RTPBukkitPlugin — the symmetric partner of diagram 06. Covers the precise ordering required to (a) stop accepting new work, (b) cancel in-flight teleports, (c) persist cached-location + player data to the SQLite-backed DatabaseAccessor, (d) shut down per-region selectors, and (e) release all outstanding chunk tickets without leaking any (S-002).
Out of scope (covered elsewhere):
- Normal per-ticket close during runtime → diagram 03.
MemoryTrackeractive-GC sweep (runtime leak safety net) → diagram 04.- Config reload (a partial teardown that reuses some of the same primitives) → diagram 09.
- Fabric / other-platform shutdown — this diagram is Bukkit-family only; Folia uses the same path with the global-region-scheduler variant of
RTPScheduler.cancelTask. - The synchronous
/reloadpitfall — seeLESSONS_LEARNED.md.
Companion prose: docs/dev/CODE_TOUR.md §14.
How to read this chart¶
- Green = the one accepting terminal (
super.onDisablereached cleanly). - Red = force-kill / cancel sinks (nothing silently swallowed — S-004 still applies).
- Blue = async / scheduler-owned work that must be drained.
- Yellow = durable-state bookkeeping (DB queues, chunk-ticket map, region maps).
- The order of the blue/yellow nodes is load-bearing. The two most common bugs — "cached locations vanished after restart" and "leaked force-loaded chunks after /reload" — are both ordering regressions (see repair lenses 3 and 9).
flowchart TD
%% Color legend: green=terminal-ok, red=kill/cancel, blue=async-drain, yellow=durable-state
classDef success fill:#c8f0c8,stroke:#225522,color:#111
classDef fail fill:#f0c8c8,stroke:#882222,color:#111
classDef async fill:#c8d8f0,stroke:#223388,color:#111
classDef data fill:#f0e8a8,stroke:#886622,color:#111
Start([Bukkit calls onDisable]):::async
CancelTimers[Cancel commandTimer and commandProcessing]:::fail
KillAsync[AsyncTeleportProcessing.kill]:::fail
KillSync[SyncTeleportProcessing.kill]:::fail
KillScan[ScanTaskProcessing.kill]:::fail
KillDb[DatabaseProcessing.kill -- stops periodic flush task]:::fail
RTPStop[RTP.stop entered]:::async
ShutDiag[diagnosticTimer.shutdown]:::async
CompleteFutures[Complete all outstanding CompletableFutures]:::async
CancelInflight[Cancel in-flight TeleportData via RTPTeleportCancel]:::fail
DbFlush1[SQL accessor flush -- WAL checkpoint]:::data
Rebuild[rebuildCachedLocationsFromMemory -- authoritative map to rows]:::data
FlushDirty[flushDirtyCache -- enqueue writes and deletes]:::data
Drain[processQueries MAX -- drain writeQueue and deleteQueue]:::async
OrderGuard{{Must happen BEFORE stop.set true -- see LESSONS_LEARNED}}:::data
StopPipes[miscAsyncTasks.stop and miscSyncTasks.stop]:::fail
CancelTracked[Cancel all trackedTasks via RTPScheduler]:::fail
PermRegions[permRegionLookup -- shutDown each then clear]:::data
TempRegions[tempRegions -- shutDown each then clear]:::data
StopFlag[databaseAccessor.stop.set true then close]:::data
CancelAgain[Re-cancel any TeleportData left not completed]:::fail
ClearProc[processingPlayers.clear]:::data
ScanKill[ScanTask.kill -- static registry clear]:::fail
Redis[networkManager.shutdown -- if present, RTPNetworkManager interface]:::fail
AccStop[serverAccessor.stop -- scheduler + platform hooks]:::fail
PendingBukkit[Cancel all RTP-owned async Bukkit tasks still pending]:::fail
RefData[Write referenceData sentinel row then processQueries MAX]:::data
ReleaseTickets[releaseAllChunkTickets -- S-002 guarantee]:::data
Super([super.onDisable -- plugin disabled]):::success
Start --> CancelTimers --> KillAsync --> KillSync --> KillScan --> KillDb --> RTPStop
RTPStop --> ShutDiag --> CompleteFutures --> CancelInflight --> DbFlush1
DbFlush1 --> Rebuild --> FlushDirty --> Drain --> OrderGuard --> StopPipes
StopPipes --> CancelTracked --> PermRegions --> TempRegions --> StopFlag
StopFlag --> CancelAgain --> ClearProc --> ScanKill --> Redis --> AccStop
AccStop --> PendingBukkit --> RefData --> ReleaseTickets ==> Super
Repair lenses¶
- "Server hang on stop" → a blue node is waiting on a future that will never complete. Check
CompleteFuturesand theprocessQueries(Long.MAX_VALUE)drain — if the DB thread already exited,processQueriesreturns fast; if it's mid-batch and another thread setstop.set(true)early, the drain dead-ends. The ordering guard betweenDrainandStopFlagexists for exactly this reason. - "Cached locations vanished after restart" → you moved or removed
rebuildCachedLocationsFromMemoryorflushDirtyCache, or reordered them relative toprocessQueries. The three must run in orderrebuild → flushDirty → processQueriesbeforestop.set(true). Regression test:MemoryShapeShutdownTest. - "Leaked force-loaded chunks after /reload or /stop" → S-002 violation.
releaseAllChunkTicketsmust be the last durable action beforesuper.onDisable. If a region'sshutDown()throws and unwinds past the release call, tickets leak. Wrap risky region teardown, don't skip the release. - "NPE during shutdown" → usually
RTP.getInstance()returned null because a prioronDisablealready ran (Bukkit can call it twice on init failure — see the two bail-out calls at lines 108/119 ofRTPBukkitPlugin). Every block is guarded bytry { ... } catch (NoClassDefFoundError ignored)or a null check for this reason; don't remove those guards. - "Teleport-in-progress player stuck after stop" →
CancelInflightdidn't fire for them.RTPTeleportCancelis invoked twice (once early, once after region shutdown) to catch data markedcompleted=falsethat was added mid-teardown. Both calls are required. - "Scan task still running after disable" →
ScanTaskProcessing.kill()cancels the tick driver, but the static registry insideScanTaskalso needsScanTask.kill(). Both are called; removing either leaves a half-dead scan. - "DB file locked on next startup" →
databaseAccessor.close()was skipped or ran beforeprocessQueriesfinished. Thestop.set(true)flag gates new work;close()releases the JDBC connection; order matters. - "Folia-only shutdown warning about region threads" →
serverAccessor.stop()(final blue-ish fail node) must run on the global region scheduler. The Folia accessor handles that internally; don't relocate the call out ofRTP.stop(). - "
referenceDatarow missing" → the post-RTP.stop()block writes a sentinel row (time + zero-UUID) so the next startup can detect a clean shutdown. It runs afterRTP.stop()so it uses the still-open DatabaseAccessor, then callsprocessQueries(MAX)one final time. Do not move it beforeRTP.stop().
Source anchors¶
rtp-plugin/src/main/java/io/github/dailystruggle/rtp/bukkit/RTPBukkitPlugin.java—onDisable()(~L184) and early bail-out calls at ~L108 and ~L119.rtp-core/src/main/java/io/github/dailystruggle/rtp/common/RTP.java—stop()(~L382).rtp-core/src/main/java/io/github/dailystruggle/rtp/common/database/DatabaseAccessor.java—stopflag,flushDirtyCache,rebuildCachedLocationsFromMemory,processQueries,close.- Regression tests:
MemoryShapeShutdownTest,RTPTest#stop_*,SyncTaskProcessingTest(shared-pipestop=truebehavior). - Background:
docs/dev/LESSONS_LEARNED.md§"Shutdown ordering",docs/dev/TRACEABILITY.mdrowREQ-CORE-NF-001.