diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed25e9452d231fe0ba5a5bc200edd83a3616104..64d449b15bbd1cb406c032483470dbd246df7937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr ### Added - New timeout option to HTTP request function in the code runtime. - Set QoS settings on client outgoing message queue. +- New runtime pool min/max size options. ### Changed - The avatar URL fields in various domain objects now support up to 512 characters for FBIG. +- Runtime modules are now loaded in a consistent order. ## [2.0.0] - 2018-05-14 diff --git a/main.go b/main.go index 03baeb240439dfbfe0c68226b90bd3445250a243..d2f550e0c8334fc23bd4ff2c0cde953149599d74 100644 --- a/main.go +++ b/main.go @@ -116,7 +116,7 @@ func main() { if err != nil { startupLogger.Fatal("Failed initializing runtime modules", zap.Error(err)) } - runtimePool := server.NewRuntimePool(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, regCallbacks, once) + runtimePool := server.NewRuntimePool(logger, startupLogger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, regCallbacks, once) pipeline := server.NewPipeline(config, db, jsonpbMarshaler, jsonpbUnmarshaler, sessionRegistry, matchRegistry, matchmaker, tracker, router, runtimePool) metrics := server.NewMetrics(logger, startupLogger, config) diff --git a/migrate/migrate-packr.go b/migrate/migrate-packr.go index ef783f3b4ee71640bd98e989ae7e0cc6760e5646..eb583c43a21c01a01cd8f3b9504e5c0b2ab5ce9f 100644 --- a/migrate/migrate-packr.go +++ b/migrate/migrate-packr.go @@ -9,5 +9,5 @@ import "github.com/gobuffalo/packr" // Go binary. You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { - packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") - } + packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") +} diff --git a/server/config.go b/server/config.go index d7a4abbbba6117002e0c071a9436b40cb41e59c1..4d2271b25d3ca97a16f1b5af3afe6623a74229ba 100644 --- a/server/config.go +++ b/server/config.go @@ -96,6 +96,15 @@ func ParseArgs(logger *zap.Logger, args []string) Config { if mainConfig.GetSocket().PingPeriodMs >= mainConfig.GetSocket().PongWaitMs { logger.Fatal("Ping period value must be less than pong wait value", zap.Int("socket.ping_period_ms", mainConfig.GetSocket().PingPeriodMs), zap.Int("socket.pong_wait_ms", mainConfig.GetSocket().PongWaitMs)) } + if mainConfig.GetRuntime().MinCount < 0 { + logger.Fatal("Minimum runtime instance count must be >= 0", zap.Int("runtime.min_count", mainConfig.GetRuntime().MinCount)) + } + if mainConfig.GetRuntime().MaxCount < 1 { + logger.Fatal("Maximum runtime instance count must be >= 1", zap.Int("runtime.max_count", mainConfig.GetRuntime().MaxCount)) + } + if mainConfig.GetRuntime().MinCount > mainConfig.GetRuntime().MaxCount { + logger.Fatal("Minimum runtime instance count must be less than or equal to maximum runtime instance count", zap.Int("runtime.min_count", mainConfig.GetRuntime().MinCount), zap.Int("runtime.max_count", mainConfig.GetRuntime().MaxCount)) + } // If the runtime path is not overridden, set it to `datadir/modules`. if mainConfig.GetRuntime().Path == "" { @@ -364,6 +373,8 @@ type RuntimeConfig struct { Env []string `yaml:"env" json:"env"` Path string `yaml:"path" json:"path" usage:"Path for the server to scan for *.lua files."` HTTPKey string `yaml:"http_key" json:"http_key" usage:"Runtime HTTP Invocation key."` + MinCount int `yaml:"min_count" json:"min_count" usage:"Minimum number of runtime instances to allocate. Default 16."` + MaxCount int `yaml:"max_count" json:"max_count" usage:"Maximum number of runtime instances to allocate. Default 65536."` } // NewRuntimeConfig creates a new RuntimeConfig struct. @@ -373,6 +384,8 @@ func NewRuntimeConfig() *RuntimeConfig { Env: make([]string, 0), Path: "", HTTPKey: "defaultkey", + MinCount: 16, + MaxCount: 65536, } } diff --git a/server/runtime.go b/server/runtime.go index c2f71c6525473d4fb4f34ab887df2e1d4b538b25..71ff9b89f993762a2829c11fd2818978f8c5f3e8 100644 --- a/server/runtime.go +++ b/server/runtime.go @@ -46,26 +46,45 @@ type RuntimeModule struct { } type RuntimePool struct { + sync.Mutex + logger *zap.Logger regCallbacks *RegCallbacks - modules *sync.Map - pool *sync.Pool + moduleCache *ModuleCache + poolCh chan *Runtime + maxCount int + currentCount int + newFn func() *Runtime } -func NewRuntimePool(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, modules *sync.Map, regCallbacks *RegCallbacks, once *sync.Once) *RuntimePool { - return &RuntimePool{ +func NewRuntimePool(logger, startupLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *ModuleCache, regCallbacks *RegCallbacks, once *sync.Once) *RuntimePool { + rp := &RuntimePool{ + logger: logger, regCallbacks: regCallbacks, - modules: modules, - pool: &sync.Pool{ - New: func() interface{} { - r, err := newVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, once, nil) - if err != nil { - logger.Fatal("Failed initializing runtime.", zap.Error(err)) - } - // TODO find a way to run r.Stop() when the pool discards this runtime. - return r - }, + moduleCache: moduleCache, + poolCh: make(chan *Runtime, config.GetRuntime().MaxCount), + maxCount: config.GetRuntime().MaxCount, + // Set the current count assuming we'll warm up the pool in a moment. + currentCount: config.GetRuntime().MinCount, + newFn: func() *Runtime { + r, err := newVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, moduleCache, once, nil) + if err != nil { + logger.Fatal("Failed initializing runtime.", zap.Error(err)) + } + return r }, } + + // Warm up the pool. + startupLogger.Info("Allocating minimum runtime pool", zap.Int("count", rp.currentCount)) + if len(moduleCache.Names) > 0 { + // Only if there are runtime modules to load. + for i := 0; i < config.GetRuntime().MinCount; i++ { + rp.poolCh <- rp.newFn() + } + } + startupLogger.Info("Allocated minimum runtime pool") + + return rp } func (rp *RuntimePool) HasCallback(mode ExecutionMode, id string) bool { @@ -85,14 +104,37 @@ func (rp *RuntimePool) HasCallback(mode ExecutionMode, id string) bool { } func (rp *RuntimePool) Get() *Runtime { - return rp.pool.Get().(*Runtime) + select { + case r := <-rp.poolCh: + // Ideally use an available idle runtime. + return r + default: + // If there was no idle runtime, see if we can allocate a new one. + rp.Lock() + if rp.currentCount >= rp.maxCount { + rp.Unlock() + // If we've reached the max allowed allocation block on an available runtime. + return <-rp.poolCh + } + // Allocate a new runtime. + rp.currentCount++ + rp.Unlock() + return rp.newFn() + } } func (rp *RuntimePool) Put(r *Runtime) { - rp.pool.Put(r) + select { + case rp.poolCh <- r: + // Runtime is successfully returned to the pool. + default: + // The pool is over capacity. Should never happen but guard anyway. + // Safe to continue processing, the runtime is just discarded. + rp.logger.Warn("Runtime pool full, discarding runtime") + } } -func newVM(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, modules *sync.Map, once *sync.Once, announceCallback func(ExecutionMode, string)) (*Runtime, error) { +func newVM(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *ModuleCache, once *sync.Once, announceCallback func(ExecutionMode, string)) (*Runtime, error) { // Initialize a one-off runtime to ensure startup code runs and modules are valid. vm := lua.NewState(lua.Options{ CallStackSize: 1024, @@ -113,13 +155,7 @@ func newVM(logger *zap.Logger, db *sql.DB, config Config, socialClient *social.C luaEnv: ConvertMap(vm, config.GetRuntime().Environment), } - mods := make([]*RuntimeModule, 0) - modules.Range(func(key interface{}, value interface{}) bool { - mods = append(mods, value.(*RuntimeModule)) - return true - }) - - return r, r.loadModules(mods) + return r, r.loadModules(moduleCache) } type Runtime struct { @@ -128,7 +164,7 @@ type Runtime struct { luaEnv *lua.LTable } -func (r *Runtime) loadModules(modules []*RuntimeModule) error { +func (r *Runtime) loadModules(moduleCache *ModuleCache) error { // `DoFile(..)` only parses and evaluates modules. Calling it multiple times, will load and eval the file multiple times. // So to make sure that we only load and evaluate modules once, regardless of whether there is dependency between files, we load them all into `preload`. // This is to make sure that modules are only loaded and evaluated once as `doFile()` does not (always) update _LOADED table. @@ -162,7 +198,11 @@ func (r *Runtime) loadModules(modules []*RuntimeModule) error { preload := r.vm.GetField(r.vm.GetField(r.vm.Get(lua.EnvironIndex), "package"), "preload") fns := make(map[string]*lua.LFunction) - for _, module := range modules { + for _, name := range moduleCache.Names { + module, ok := moduleCache.Modules[name] + if !ok { + r.logger.Fatal("Failed to find named module in cache", zap.String("name", name)) + } f, err := r.vm.Load(bytes.NewReader(module.Content), module.Path) if err != nil { r.logger.Error("Could not load module", zap.String("name", module.Path), zap.Error(err)) @@ -173,7 +213,11 @@ func (r *Runtime) loadModules(modules []*RuntimeModule) error { } } - for name, fn := range fns { + for _, name := range moduleCache.Names { + fn, ok := fns[name] + if !ok { + r.logger.Fatal("Failed to find named module in prepared functions", zap.String("name", name)) + } loaded := r.vm.GetField(r.vm.Get(lua.RegistryIndex), "_LOADED") lv := r.vm.GetField(loaded, name) if lua.LVAsBool(lv) { diff --git a/server/runtime_loadlib.go b/server/runtime_loadlib.go index fd298169902a1ff1b34bbc35e67cda32dbfb9084..aea9437f7b25d460d003ba8ad45b78f9b1a9145b 100644 --- a/server/runtime_loadlib.go +++ b/server/runtime_loadlib.go @@ -25,12 +25,10 @@ package server import ( "bytes" "fmt" + "github.com/yuin/gopher-lua" "os" "path/filepath" "strings" - "sync" - - "github.com/yuin/gopher-lua" ) const emptyLString lua.LString = lua.LString("") @@ -51,20 +49,15 @@ func loGetPath(env string, defpath string) string { return path } -func OpenPackage(modules *sync.Map) func(L *lua.LState) int { +func OpenPackage(moduleCache *ModuleCache) func(L *lua.LState) int { return func(L *lua.LState) int { loLoaderCache := func(L *lua.LState) int { name := L.CheckString(1) - m, ok := modules.Load(name) + module, ok := moduleCache.Modules[name] if !ok { L.Push(lua.LString(fmt.Sprintf("no cached module '%s'", name))) return 1 } - module, ok := m.(*RuntimeModule) - if !ok { - L.Push(lua.LString(fmt.Sprintf("invalid cached module '%s'", name))) - return 1 - } fn, err := L.Load(bytes.NewReader(module.Content), module.Path) if err != nil { L.RaiseError(err.Error()) diff --git a/server/runtime_module_cache.go b/server/runtime_module_cache.go index 5e1392034a0d0fe961b63c04c3fe8e3b7d6ccf2b..f2239901baeeb0003f8198dec47f6fabb9a20eeb 100644 --- a/server/runtime_module_cache.go +++ b/server/runtime_module_cache.go @@ -25,8 +25,14 @@ import ( "github.com/heroiclabs/nakama/social" "github.com/yuin/gopher-lua" "go.uber.org/zap" + "sort" ) +type ModuleCache struct { + Names []string + Modules map[string]*RuntimeModule +} + type RegCallbacks struct { RPC map[string]interface{} Before map[string]interface{} @@ -34,13 +40,16 @@ type RegCallbacks struct { Matchmaker interface{} } -func LoadRuntimeModules(startupLogger *zap.Logger, config Config) (map[string]lua.LGFunction, *sync.Map, error) { +func LoadRuntimeModules(startupLogger *zap.Logger, config Config) (map[string]lua.LGFunction, *ModuleCache, error) { runtimeConfig := config.GetRuntime() if err := os.MkdirAll(runtimeConfig.Path, os.ModePerm); err != nil { return nil, nil, err } - modules := new(sync.Map) + moduleCache := &ModuleCache{ + Names: make([]string, 0), + Modules: make(map[string]*RuntimeModule, 0), + } // Override before Package library is invoked. lua.LuaLDir = runtimeConfig.Path @@ -64,11 +73,12 @@ func LoadRuntimeModules(startupLogger *zap.Logger, config Config) (map[string]lu name := strings.TrimSuffix(relPath, filepath.Ext(relPath)) // Make paths Lua friendly. name = strings.Replace(name, "/", ".", -1) - modules.Store(name, &RuntimeModule{ + moduleCache.Names = append(moduleCache.Names, name) + moduleCache.Modules[name] = &RuntimeModule{ Name: name, Path: path, Content: content, - }) + } modulePaths = append(modulePaths, relPath) } } @@ -78,8 +88,11 @@ func LoadRuntimeModules(startupLogger *zap.Logger, config Config) (map[string]lu return nil, nil, err } + // Ensure modules will be listed in ascending order of names. + sort.Strings(moduleCache.Names) + stdLibs := map[string]lua.LGFunction{ - lua.LoadLibName: OpenPackage(modules), + lua.LoadLibName: OpenPackage(moduleCache), lua.BaseLibName: lua.OpenBase, lua.TabLibName: lua.OpenTable, lua.OsLibName: OpenOs, @@ -89,10 +102,10 @@ func LoadRuntimeModules(startupLogger *zap.Logger, config Config) (map[string]lu startupLogger.Info("Found runtime modules", zap.Int("count", len(modulePaths)), zap.Strings("modules", modulePaths)) - return stdLibs, modules, nil + return stdLibs, moduleCache, nil } -func ValidateRuntimeModules(logger, startupLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, modules *sync.Map, once *sync.Once) (*RegCallbacks, error) { +func ValidateRuntimeModules(logger, startupLogger *zap.Logger, db *sql.DB, config Config, socialClient *social.Client, leaderboardCache LeaderboardCache, sessionRegistry *SessionRegistry, matchRegistry MatchRegistry, tracker Tracker, router MessageRouter, stdLibs map[string]lua.LGFunction, moduleCache *ModuleCache, once *sync.Once) (*RegCallbacks, error) { regCallbacks := &RegCallbacks{ RPC: make(map[string]interface{}), Before: make(map[string]interface{}), @@ -100,7 +113,7 @@ func ValidateRuntimeModules(logger, startupLogger *zap.Logger, db *sql.DB, confi } startupLogger.Info("Evaluating runtime modules") - r, err := newVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, once, func(execMode ExecutionMode, id string) { + r, err := newVM(logger, db, config, socialClient, leaderboardCache, sessionRegistry, matchRegistry, tracker, router, stdLibs, moduleCache, once, func(execMode ExecutionMode, id string) { switch execMode { case ExecutionModeRPC: regCallbacks.RPC[id] = struct{}{}