refactor(game): lock-free storage, remove /command, flatten engine wrapper
Three-stage refactor of the game-engine plumbing (game logic untouched): Stage 1 — lock-free persistence + admin serialisation. Remove the file lock from repo/fs (the .lock file, the Read/Write-vs-*Safe duality and the dead ReadSafe polling) and replace the two-step rename with a single atomic rename so concurrent reads are torn-free without a lock. Serialise the state-mutating admin writers (init/turn/banish) with one shared router LimitMiddleware, rewritten to block on the request context instead of a racy shared 100ms timer. Stage 2 — remove the obsolete immediate-command path end to end. Players submit through PUT /api/v1/order; the legacy PUT /api/v1/command path is deleted across game (route, handler, 24 command factories, Ctrl), backend (Commands handler/route, engineclient.ExecuteCommands), gateway (dispatch + executeUserGamesCommand + routing entry), the FlatBuffers/model contract (UserGamesCommand[Response]) and transcoder, plus every affected OpenAPI/README/FUNCTIONAL/ARCHITECTURE doc. The integration proxy test is converted to the order path. Stage 3 — flatten the REST->engine wrapper. Replace the executor adapter, the controller package functions and RepoController with one concrete controller.Service; drop the single-implementation Repo and Storage interfaces (repo.Repo / fs.FS are now concrete). Handlers depend on a thin handler.Engine seam and own the domain->REST projection; storage is resolved once at startup instead of per request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-42
@@ -19,6 +19,7 @@ import (
|
||||
"galaxy/model/report"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
"galaxy/game/internal/repo/fs"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -42,11 +43,11 @@ func (o *storedOrder) UnmarshalBinary(data []byte) error {
|
||||
return json.Unmarshal(data, o)
|
||||
}
|
||||
|
||||
func (r *repo) SaveNewTurn(t uint, g *game.Game) error {
|
||||
func (r *Repo) SaveNewTurn(t uint, g *game.Game) error {
|
||||
return saveNewTurn(r.s, t, g)
|
||||
}
|
||||
|
||||
func saveNewTurn(s Storage, t uint, g *game.Game) error {
|
||||
func saveNewTurn(s *fs.FS, t uint, g *game.Game) error {
|
||||
path := fmt.Sprintf("%s/state.json", TurnDir(t))
|
||||
exist, err := s.Exists(path)
|
||||
if err != nil {
|
||||
@@ -61,27 +62,23 @@ func saveNewTurn(s Storage, t uint, g *game.Game) error {
|
||||
return saveLastState(s, g)
|
||||
}
|
||||
|
||||
func (r *repo) SaveLastState(g *game.Game) error {
|
||||
func (r *Repo) SaveLastState(g *game.Game) error {
|
||||
return saveLastState(r.s, g)
|
||||
}
|
||||
|
||||
func saveLastState(s Storage, g *game.Game) error {
|
||||
func saveLastState(s *fs.FS, g *game.Game) error {
|
||||
if err := s.Write(statePath, g); err != nil {
|
||||
return NewStorageError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repo) LoadState() (*game.Game, error) {
|
||||
return loadState(r.s, true)
|
||||
func (r *Repo) LoadState() (*game.Game, error) {
|
||||
return loadState(r.s)
|
||||
}
|
||||
|
||||
func (r *repo) LoadStateSafe() (*game.Game, error) {
|
||||
return loadState(r.s, false)
|
||||
}
|
||||
|
||||
func loadState(s Storage, locked bool) (*game.Game, error) {
|
||||
var result *game.Game = new(game.Game)
|
||||
func loadState(s *fs.FS) (*game.Game, error) {
|
||||
result := new(game.Game)
|
||||
path := statePath
|
||||
exist, err := s.Exists(path)
|
||||
if err != nil {
|
||||
@@ -90,19 +87,13 @@ func loadState(s Storage, locked bool) (*game.Game, error) {
|
||||
if !exist {
|
||||
return nil, NewGameNotInitializedError()
|
||||
}
|
||||
if locked {
|
||||
if err := s.Read(path, result); err != nil {
|
||||
return nil, NewStorageError(err)
|
||||
}
|
||||
} else {
|
||||
if err := s.ReadSafe(path, result); err != nil {
|
||||
return nil, NewStorageError(err)
|
||||
}
|
||||
if err := s.Read(path, result); err != nil {
|
||||
return nil, NewStorageError(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadMeta(s Storage) (*game.GameMeta, error) {
|
||||
func loadMeta(s *fs.FS) (*game.GameMeta, error) {
|
||||
var result *game.GameMeta = new(game.GameMeta)
|
||||
path := metaPath
|
||||
exist, err := s.Exists(path)
|
||||
@@ -112,13 +103,13 @@ func loadMeta(s Storage) (*game.GameMeta, error) {
|
||||
if !exist {
|
||||
return result, nil
|
||||
}
|
||||
if err := s.ReadSafe(path, result); err != nil {
|
||||
if err := s.Read(path, result); err != nil {
|
||||
return nil, NewStorageError(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadTurnMeta(s Storage, turn uint) (*game.GameMeta, error) {
|
||||
func loadTurnMeta(s *fs.FS, turn uint) (*game.GameMeta, error) {
|
||||
var result *game.GameMeta = new(game.GameMeta)
|
||||
path := fmt.Sprintf("%s/%s", TurnDir(turn), metaPath)
|
||||
exist, err := s.Exists(path)
|
||||
@@ -128,13 +119,13 @@ func loadTurnMeta(s Storage, turn uint) (*game.GameMeta, error) {
|
||||
if !exist {
|
||||
return result, nil
|
||||
}
|
||||
if err := s.ReadSafe(path, result); err != nil {
|
||||
if err := s.Read(path, result); err != nil {
|
||||
return nil, NewStorageError(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func saveMeta(s Storage, turn uint, gm *game.GameMeta) error {
|
||||
func saveMeta(s *fs.FS, turn uint, gm *game.GameMeta) error {
|
||||
// save turn's meta
|
||||
path := fmt.Sprintf("%s/%s", TurnDir(turn), metaPath)
|
||||
if err := s.Write(path, gm); err != nil {
|
||||
@@ -148,7 +139,7 @@ func saveMeta(s Storage, turn uint, gm *game.GameMeta) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repo) LoadBattle(turn uint, id uuid.UUID) (*report.BattleReport, bool, error) {
|
||||
func (r *Repo) LoadBattle(turn uint, id uuid.UUID) (*report.BattleReport, bool, error) {
|
||||
meta, err := loadTurnMeta(r.s, turn)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -164,7 +155,7 @@ func (r *repo) LoadBattle(turn uint, id uuid.UUID) (*report.BattleReport, bool,
|
||||
return result, true, nil
|
||||
}
|
||||
|
||||
func (r *repo) SaveBattle(turn uint, b *report.BattleReport, m *game.BattleMeta) error {
|
||||
func (r *Repo) SaveBattle(turn uint, b *report.BattleReport, m *game.BattleMeta) error {
|
||||
meta, err := loadMeta(r.s)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -177,7 +168,7 @@ func (r *repo) SaveBattle(turn uint, b *report.BattleReport, m *game.BattleMeta)
|
||||
return saveMeta(r.s, turn, meta)
|
||||
}
|
||||
|
||||
func saveBattle(s Storage, turn uint, b *report.BattleReport) error {
|
||||
func saveBattle(s *fs.FS, turn uint, b *report.BattleReport) error {
|
||||
path := fmt.Sprintf("%s/battle/%s.json", TurnDir(turn), b.ID.String())
|
||||
exist, err := s.Exists(path)
|
||||
if err != nil {
|
||||
@@ -192,7 +183,7 @@ func saveBattle(s Storage, turn uint, b *report.BattleReport) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadBattle(s Storage, turn uint, id uuid.UUID) (*report.BattleReport, error) {
|
||||
func loadBattle(s *fs.FS, turn uint, id uuid.UUID) (*report.BattleReport, error) {
|
||||
path := fmt.Sprintf("%s/battle/%s.json", TurnDir(turn), id.String())
|
||||
exist, err := s.Exists(path)
|
||||
if err != nil {
|
||||
@@ -202,13 +193,13 @@ func loadBattle(s Storage, turn uint, id uuid.UUID) (*report.BattleReport, error
|
||||
return nil, NewStateError(fmt.Sprintf("battle %v for turn %d never was saved", id, turn))
|
||||
}
|
||||
result := new(report.BattleReport)
|
||||
if err := s.ReadSafe(path, result); err != nil {
|
||||
if err := s.Read(path, result); err != nil {
|
||||
return nil, NewStorageError(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *repo) SaveBombings(turn uint, b []*game.Bombing) error {
|
||||
func (r *Repo) SaveBombings(turn uint, b []*game.Bombing) error {
|
||||
meta, err := loadMeta(r.s)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -219,11 +210,11 @@ func (r *repo) SaveBombings(turn uint, b []*game.Bombing) error {
|
||||
return saveMeta(r.s, turn, meta)
|
||||
}
|
||||
|
||||
func (r *repo) SaveReport(turn uint, rep *report.Report) error {
|
||||
func (r *Repo) SaveReport(turn uint, rep *report.Report) error {
|
||||
return saveReport(r.s, turn, rep)
|
||||
}
|
||||
|
||||
func saveReport(s Storage, t uint, v *report.Report) error {
|
||||
func saveReport(s *fs.FS, t uint, v *report.Report) error {
|
||||
path := ReportDir(t, v.RaceID)
|
||||
if err := s.Write(path, v); err != nil {
|
||||
return NewStorageError(err)
|
||||
@@ -231,11 +222,11 @@ func saveReport(s Storage, t uint, v *report.Report) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repo) LoadReport(turn uint, id uuid.UUID) (*report.Report, error) {
|
||||
func (r *Repo) LoadReport(turn uint, id uuid.UUID) (*report.Report, error) {
|
||||
return loadReport(r.s, turn, id)
|
||||
}
|
||||
|
||||
func loadReport(s Storage, turn uint, id uuid.UUID) (*report.Report, error) {
|
||||
func loadReport(s *fs.FS, turn uint, id uuid.UUID) (*report.Report, error) {
|
||||
path := ReportDir(turn, id)
|
||||
result := new(report.Report)
|
||||
exist, err := s.Exists(path)
|
||||
@@ -245,29 +236,29 @@ func loadReport(s Storage, turn uint, id uuid.UUID) (*report.Report, error) {
|
||||
if !exist {
|
||||
return nil, NewReportNotFoundError()
|
||||
}
|
||||
if err := s.ReadSafe(path, result); err != nil {
|
||||
if err := s.Read(path, result); err != nil {
|
||||
return nil, NewStorageError(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *repo) SaveOrder(t uint, id uuid.UUID, o *order.UserGamesOrder) error {
|
||||
func (r *Repo) SaveOrder(t uint, id uuid.UUID, o *order.UserGamesOrder) error {
|
||||
return saveOrder(r.s, t, id, o)
|
||||
}
|
||||
|
||||
func saveOrder(s Storage, t uint, id uuid.UUID, o *order.UserGamesOrder) error {
|
||||
func saveOrder(s *fs.FS, t uint, id uuid.UUID, o *order.UserGamesOrder) error {
|
||||
path := OrderDir(t, id)
|
||||
if err := s.WriteSafe(path, o); err != nil {
|
||||
if err := s.Write(path, o); err != nil {
|
||||
return NewStorageError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *repo) LoadOrder(t uint, id uuid.UUID) (*order.UserGamesOrder, bool, error) {
|
||||
func (r *Repo) LoadOrder(t uint, id uuid.UUID) (*order.UserGamesOrder, bool, error) {
|
||||
return loadOrder(r.s, t, id)
|
||||
}
|
||||
|
||||
func loadOrder(s Storage, t uint, id uuid.UUID) (*order.UserGamesOrder, bool, error) {
|
||||
func loadOrder(s *fs.FS, t uint, id uuid.UUID) (*order.UserGamesOrder, bool, error) {
|
||||
path := OrderDir(t, id)
|
||||
|
||||
exist, err := s.Exists(path)
|
||||
@@ -279,7 +270,7 @@ func loadOrder(s Storage, t uint, id uuid.UUID) (*order.UserGamesOrder, bool, er
|
||||
}
|
||||
|
||||
stored := new(storedOrder)
|
||||
if err := s.ReadSafe(path, stored); err != nil {
|
||||
if err := s.Read(path, stored); err != nil {
|
||||
return nil, false, NewStorageError(err)
|
||||
}
|
||||
// An empty stored batch is a valid state — the player either
|
||||
|
||||
Reference in New Issue
Block a user