feat(shutdown): passive drain and rename DisableSoftCrash

Shutdown now proceeds in three phases: close listeners immediately on
signal, broadcast the in-game countdown, wait up to ShutdownDrainSeconds
(default 30) for sessions to disconnect naturally via DrainPassive, then
force-close any stragglers. This prevents players from entering new
quests after the countdown starts, and lets mid-quest sessions finish
saving without being killed mid-write. A second SIGINT during passive
drain cancels it so the force-close phase runs immediately.

Renamed DisableSoftCrash -> DisableShutdownCountdown since the flag
controls the countdown, not crash behaviour. Existing config.json files
keep working via a Viper alias on the legacy key.

Closes #179.
This commit is contained in:
Houmgaor
2026-04-06 19:32:35 +02:00
parent 9b0f735335
commit e48d33ca76
7 changed files with 134 additions and 64 deletions

View File

@@ -249,6 +249,30 @@ func (s *Server) Shutdown() {
}
// DrainPassive waits for active sessions to disconnect naturally (e.g. players
// finishing a quest and logging out) without force-closing anything. Returns
// when the session count reaches zero or ctx is cancelled. Callers should have
// already invoked Shutdown() to close the listener so no new sessions arrive.
func (s *Server) DrainPassive(ctx context.Context) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
s.Lock()
n := len(s.sessions)
s.Unlock()
if n == 0 {
s.logger.Info("Passive drain complete: all sessions disconnected")
return
}
select {
case <-ctx.Done():
s.logger.Info("Passive drain deadline reached", zap.Int("remaining_sessions", n))
return
case <-ticker.C:
}
}
}
// ShutdownAndDrain stops accepting new connections, force-closes every active
// session so that their logoutPlayer cleanup runs (saves character data, removes
// from stages, etc.), then waits until all sessions have been removed from the