48614e622d
The dictionary lives on a persistent volume seeded from the image once and never re-seeded, so a redeploy that bumped BACKEND_DICT_VERSION left the new DAWGs unreachable (the volume mount shadows the image copy) and the active version stuck at whatever the admin console last installed — a native offline-first client then fetched the server's older pinned version over the network instead of using its bundled dict. On boot the backend now DELIVERS the build version onto the volume add-only, from a second unshadowed image copy at BACKEND_DICT_SEED_DIR, into DICT_DIR/<version>/ when absent (the flat seed and prior uploads untouched, so in-flight games keep theirs), and InitActiveVersion makes the build version active — except a console-installed version NEWER than the build (compared numerically) is not downgraded by a restart. The .seed_version guard still protects the flat seed's label (a bump is a new subdirectory, never a relabel). The console stays for out-of-band updates. Docs: ARCHITECTURE.md §5, deploy/README.md, seedmarker.go. Tests: engine.DeliverVersion (add-only, idempotent, flat-seed skip), compareDictVersions, and the dictionary-update integration test's deploy-delivers path.
62 lines
2.3 KiB
Go
62 lines
2.3 KiB
Go
package game
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Config configures the game subsystem: where the engine loads its dictionaries,
|
|
// which version new games pin, and the two background knobs — the turn-timeout
|
|
// sweep cadence and the idle window after which the live-game cache evicts a game
|
|
// (it is then rebuilt from the journal on next access, so this never affects
|
|
// correctness). It composes into the backend configuration.
|
|
type Config struct {
|
|
// DictDir is the directory holding the committed DAWG files. Sourced from
|
|
// BACKEND_DICT_DIR; it has no default and must be set.
|
|
DictDir string
|
|
// DictVersion labels the dictionary version new games pin. Sourced from
|
|
// BACKEND_DICT_VERSION.
|
|
DictVersion string
|
|
// DictSeedDir holds the image's read-only copy of the build's DictVersion DAWGs,
|
|
// on a path the persistent DictDir volume does NOT shadow. On boot the backend
|
|
// delivers this version into DictDir/<DictVersion>/ if absent (add-only), so a
|
|
// redeploy that bumped DICT_VERSION makes the new dictionary resident and activatable
|
|
// without disturbing the versions in-flight games already pin. Sourced from
|
|
// BACKEND_DICT_SEED_DIR; empty disables delivery (the pre-existing seed-only behaviour).
|
|
DictSeedDir string
|
|
// TimeoutSweepInterval is how often the sweeper scans for overdue turns.
|
|
// Sourced from BACKEND_GAME_TIMEOUT_SWEEP_INTERVAL.
|
|
TimeoutSweepInterval time.Duration
|
|
// CacheTTL is how long an idle game stays resident before eviction. Sourced
|
|
// from BACKEND_GAME_CACHE_TTL.
|
|
CacheTTL time.Duration
|
|
}
|
|
|
|
// DefaultConfig returns the game configuration defaults. DictDir is deliberately
|
|
// empty: it must be supplied through the environment.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
DictVersion: "v1",
|
|
TimeoutSweepInterval: time.Minute,
|
|
CacheTTL: 24 * time.Hour,
|
|
}
|
|
}
|
|
|
|
// Validate reports whether the configuration is usable.
|
|
func (c Config) Validate() error {
|
|
if c.DictDir == "" {
|
|
return errors.New("game: BACKEND_DICT_DIR must be set")
|
|
}
|
|
if c.DictVersion == "" {
|
|
return errors.New("game: BACKEND_DICT_VERSION must not be empty")
|
|
}
|
|
if c.TimeoutSweepInterval <= 0 {
|
|
return fmt.Errorf("game: timeout sweep interval must be positive, got %s", c.TimeoutSweepInterval)
|
|
}
|
|
if c.CacheTTL <= 0 {
|
|
return fmt.Errorf("game: cache TTL must be positive, got %s", c.CacheTTL)
|
|
}
|
|
return nil
|
|
}
|