Commit f434f3da authored by Andrei Mihu's avatar Andrei Mihu
Browse files

Graceful server shutdown and match termination. (#246)

parent d79ccd1a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
- Lua runtime AES-256 functions.
- Lua runtime token generator function now returns a second value representing the token's expiry.
- Add local cache for in-memory storage to the Lua runtime.
- Graceful server shutdown and match termination.

### Changed
- Improved Postgres compatibility on TIMESTAMPTZ types.
+46 −1
Original line number Diff line number Diff line
@@ -262,11 +262,56 @@ local function match_loop(context, dispatcher, tick, state, messages)
  end
end

--[[
Called when the server begins a graceful shutdown process. Will not be called if graceful shutdown is disabled.

Context represents information about the match and server, for information purposes. Format:
{
  env = {}, -- key-value data set in the runtime.env server configuration.
  executionMode = "Match",
  match_id = "client-friendly match ID, can be shared with clients and used in match join operations",
  match_node = "name of the Nakama node hosting this match",
  match_label = "the label string returned from match_init",
  match_tick_rate = 1 -- the tick rate returned by match_init
}

Dispatcher exposes useful functions to the match. Format:
{
  broadcast_message = function(op_code, data, presences, sender),
    -- numeric message op code
    -- a data payload string, or nil
    -- list of presences (a subset of match participants) to use as message targets, or nil to send to the whole match
    -- a presence to tag on the message as the 'sender', or nil
  match_kick = function(presences)
    -- a list of presences to remove from the match
  match_label_update = function(label)
    -- a new label to set for the match
}

Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with
calls to match_join_attempt, match_join, or match_leave.

State is the current in-memory match state, may be any Lua term except nil.

Grace Seconds is the number of seconds remaining until the server will shut down.

Expected return these values (all required) in order:
1. An (optionally) updated state. May be any non-nil Lua term, or nil to end the match.
--]]
local function match_terminate(context, dispatcher, tick, state, grace_seconds)
  if state.debug then
    print("match " .. context.match_id .. " tick " .. tick)
    print("match " .. context.match_id .. " grace_seconds " .. grace_seconds)
  end
  return state
end

-- Match modules must return a table with these functions defined. All functions are required.
return {
  match_init = match_init,
  match_join_attempt = match_join_attempt,
  match_join = match_join,
  match_leave = match_leave,
  match_loop = match_loop
  match_loop = match_loop,
  match_terminate = match_terminate
}
+34 −3
Original line number Diff line number Diff line
@@ -138,14 +138,43 @@ func main() {

	// Wait for a termination signal.
	<-c
	startupLogger.Info("Shutting down")

	// Gracefully stop server components.
	graceSeconds := config.GetShutdownGraceSec()

	// If a shutdown grace period is allowed, prepare a timer.
	var timer *time.Timer
	timerCh := make(<-chan time.Time, 1)
	if graceSeconds != 0 {
		timer = time.NewTimer(time.Duration(graceSeconds) * time.Second)
		timerCh = timer.C
		startupLogger.Info("Shutdown started - use CTRL^C to force stop server", zap.Int("grace_period_sec", graceSeconds))
	} else {
		// No grace period.
		startupLogger.Info("Shutdown started")
	}

	// Stop any running authoritative matches and do not accept any new ones.
	select {
	case <-matchRegistry.Stop(graceSeconds):
		// Graceful shutdown has completed.
	case <-timerCh:
		// Timer has expired, terminate matches immediately.
		startupLogger.Info("Shutdown grace period expired")
		<-matchRegistry.Stop(0)
	case <-c:
		// A second interrupt has been received.
		startupLogger.Info("Skipping graceful shutdown")
		<-matchRegistry.Stop(0)
	}
	if timer != nil {
		timer.Stop()
	}

	// Gracefully stop remaining server components.
	apiServer.Stop()
	consoleServer.Stop()
	metrics.Stop(logger)
	leaderboardScheduler.Stop()
	matchRegistry.Stop()
	tracker.Stop()
	sessionRegistry.Stop()

@@ -153,6 +182,8 @@ func main() {
		ga.SendSessionStop(telemetryClient, gacode, cookie)
	}

	startupLogger.Info("Shutdown complete")

	os.Exit(0)
}

+1 −0
Original line number Diff line number Diff line
@@ -220,6 +220,7 @@ type Match interface {
	MatchJoin(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, presences []Presence) interface{}
	MatchLeave(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, presences []Presence) interface{}
	MatchLoop(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, messages []MatchData) interface{}
	MatchTerminate(ctx context.Context, logger *log.Logger, db *sql.DB, nk NakamaModule, dispatcher MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{}
}

type NotificationSend struct {
+9 −0
Original line number Diff line number Diff line
@@ -105,3 +105,12 @@ func (m *Match) MatchLoop(ctx context.Context, logger *log.Logger, db *sql.DB, n
	}
	return state
}

func (m *Match) MatchTerminate(ctx context.Context, logger *log.Logger, db *sql.DB, nk runtime.NakamaModule, dispatcher runtime.MatchDispatcher, tick int64, state interface{}, graceSeconds int) interface{} {
	if state.(*MatchState).debug {
		logger.Printf("match terminate match_id %v tick %v", ctx.Value(runtime.RUNTIME_CTX_MATCH_ID), tick)
		logger.Printf("match terminate match_id %v grace seconds %v", ctx.Value(runtime.RUNTIME_CTX_MATCH_ID), graceSeconds)
	}

	return state
}
Loading