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

Finer control over runtime pooling behaviour. (#207)

parent 8282b0c5
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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

+1 −1
Original line number Diff line number Diff line
@@ -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)

+2 −2
Original line number Diff line number Diff line
@@ -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", "\"H4sIAAAAAAAA/7xZYXPbNtL+rl+x4w+vpb60LTtJ20nam6ElOuFVoXKi1Cb3hQORawk1SbAAJFl3c//9BiApESIl0b7euTONSC4Wi91nHywWN9914DsYsGzL6WIp4a5/+yNMlwgeeSIJAXsll4yLDmi5EQ0xFRjBKo2Qg1wi2BkJl1h+seBX5IKyFO6u+9BVAhfFp4veB6Viy1aQkC2kTMJKIMglFfBIYwR8DjGTQFMIWZLFlKQhwobKpZ6n0HKtdHwrdLC5JDQFAiHLtsAeq4JAZGH0Usrs/c3NZrO5JtrYa8YXN3EuJm5G7sDxfOfq7rpfDJilMQoBHP9YUY4RzLdAsiymIZnHCDHZAONAFhwxAsmUwRtOJU0XFgj2KDeEo1ITUSE5na+k4a/SPCoMAZYCSeHC9sH1L+De9l3fUkp+c6efxrMp/GZPJrY3dR0fxhMYjL2hO3XHng/jB7C9b/CL6w0tQCqXyAGfM65WwDhQ5UmMtNt8RMOER5abJDIM6SMNISbpYkUWCAu2Rp7SdAEZ8oQKFVEBJI2UmpgmVBKpX9XWpSa66XSuruD/E7rgRCLMss5g4thTB6b2/cgB9wG88RScr64/9RUGuIBuBwDgy8T9bE++wS/ON+jSqGd19GsaQeVvNnOH+yelyZuNRpaWVMpSkmD+7Vd7MvhkT7q3dz/2QPnMn05s15vmcwalcPCEW5h57t9mzoG6iIosJtsgV1mqu3v3rpd/J2siCQ9WPK5Ot/9+daXBJ97f3EjGYnFNUT5q9C1lEt/Mw+ztD1pQOT6QZHFgtzIbhs6DPRtN4RLTy1xtzELtflNaT6umxOvFNVz4JIUHTtKQipBZMLAv9FhJE/wHS/Hk2C8kx8OUJgjdmQ//BwOSkoj0ciUJShIRSXIlf/XH3v0uIDtz//mvywN3bkgcoywFWw/DhNB4J1g1GYqw5XIZEWLDeAGW+29Tx96NGnxyBr9AN8Z0IZfdUrIHP8Gbu36/X8TrkYQ4Z+wp0Igz4VOdacHYIsagwOUJOZJgiKlErmSPywmJJCnVnZALV0Ky5Py8GC0wCNkq1c5WiIeao/ulTyrCf/kZ+r0D74ccicRA4QYApu5nx5/an79M/17RlbJN93DcKoteNW6NnD5uT48b2P602wfbr34/VBRRofi60PQqRZ3eB01lPspVBv5WSEw0eVx3XM93JlPl23FJYTSydgTU6+QRGs0cH7qX/eLvquF/5d+lBZeX+bixp9jqYeQOpooGYThWJn1yvY8fOue4NIhwTUM8yqjq7cN44rgfvfytHqRmmTgPzsTxBo6/X1FP2TJ0Rs7UUc4a2EOngZRNJDaRcolZk7zB9PS5lSmkNq1LsBUPVTpaICSRaEHGBFUM2bTenXQvN6H1smuaIhSSppqKX+HAnR0N25rpwHI1xcd79+M+pXeSir1nqjRTu7pgXALjam9mKXC2EdcNSWkmxamkNFd6ylYdgH18/c/2aFRau6cew+pHTjGNuv2eBTRdU4nd293PqHvXs2Aes/AJo+6bngURxqjev+1ZQHi4pGuMuu96uU+LXbyKiIMgnQNayqTa+vL9tVvu4g/u18/OewhZ+MQZCZeXqhgi8VYgV4WcqmXDGNeq9ErZarGEzRJTgzqXRMDQ8QeQsAhVUFTNRNMIn6/rgC4yxqpqsOC/lr6HSVmplKruCGh0vFRqk+QFQlbz3zGUBnHkm/mhZMhSifkedlAtnKwXQuXhJvwdpouHCyLpGmFN4hUKIBxB5CTPUSBf67pZmYyqzM2Xd3px1ZCbW86xBDsHSCEZJ828F7I4xlCFxgKOJLLgCbdWGYo/ESz7ic5xvcJH+fef7wpFRaCiU3x5AQzWxUG0asqbu14dBkn0TqXnUh0g87mYRmgee+XYY2gqZ78taykt3FRFqQMittWSC58rxtrBq16MtR1nEGoVagbIFHwL9Lre0Pl6gN79uED5JiiGKR4JaPSsELcDeB3OOx58wu3peXTcggVND5TOfNf7CB9dD7pa5EyyJShEmWxmiSG5KtEVeasiQz8URLZ7jlCEnGaS8d2rmMwxbqZxsyYpCeZlmWkejGuZ1EDUV1cQLonUm636EeTI0DuufuaYsDXqXXfB2SoLfmc01Rtv/kiifOvNn2Ika+y+2z0/0fCp+/3uMeMsYWqz/kHVtVVebsiEhuqgY1JvwxKroubhv8ZAhtZ9OJtMaRCt7FpHDajh4KyoxscZW6u7YAMJHmVA8+AG7cu9gyKx7cBOpfwqg6bRfm6Hi5FEyOeM8KieeGUX6ADqR71FdL9S96fWCHA/Ho8c2zNd9WCPfF3PqzI5yMvko3C8NTcLIkKdPCrI3VsFa5YhJyrY7TCtlMxR5CkoUBbFbsgxwVTqxIuwfHqjJlC1iAxEuMRoFeNu6d+/rbRsQs5SVfknRL6Hi++g8t9F57Bh8zoAtQDBCyIdcAxZU8CrMgpA+JxRvi24U4SMq39W8+IX26T7YsegVFOPwasG4I6zq6nhOORKG17IT1V9GoxqRXuEG6e8WtMmF66XCKVn2ikphet60lUSVBQZLaRavbIXriv6HyGvgbdajqrg6+ios/2mCveVaLDgBJLPcmLeKg1ijBbI60ly8oD4goq/iVqr+G2oIKp1+4G0EcIlSReKuOpxbwmPloB4FR5eR1966ZIlcyFZ2lAkGsHYn8JOO63BxS9e7TnDdTUm6gZXW6TW7hLCqvSPC1AdosQ8sB1bAms8Mhs0Uy3XzGZApQmRmx+cuqspay7jiFpexBjXNIcfW97RGDc0py9oDEy3vOUw2mbmAbFO21r2gGrVOliGaV7Zx0xglJcnxl3Ai64CbsH2htXxP/0MCXnOHw5Yvnxd2yz2u0V/N81eWs3SO479trcGRqa3HWTcD7z6euDEmXTvuaBioDr9FofUMi3NNKwmX2WczsST81Unqcx9bj5jjoPMb0Er+658+558p333+8/qfb+uOGjb935F11us1LkhSmiesvkvdRZIMJkj1wcBdfYOOP6xUueFN9WG99uecd463e6uXsYP2SbtDCfjL/uI1qL54YSAOPLR3BuPCBlVzRGZ+hnhvOARiaKbc+Rr0SI68rXa+D615BP+qtzGnZAQHzr/DgAA//+3TGWchCMAAA==\"")
	packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"")
}
+13 −0
Original line number Diff line number Diff line
@@ -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,
	}
}

+71 −27
Original line number Diff line number Diff line
@@ -46,27 +46,46 @@ 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)
		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))
			}
				// TODO find a way to run r.Stop() when the pool discards this runtime.
			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 {
	ok := false
@@ -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) {
Loading