Files
scrabble-game/backend/internal/game/config.go
T
Ilia Denisov 48614e622d feat(dict): make the build's dictionary version deploy-authoritative
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.
2026-07-15 00:21:20 +02:00

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
}