feat: gamemaster
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
// Package ports defines the stable interfaces that connect Game Master
|
||||
// use cases to external state and external services.
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/gamemaster/internal/domain/runtime"
|
||||
)
|
||||
|
||||
//go:generate go run go.uber.org/mock/mockgen -destination=../adapters/mocks/mock_runtimerecordstore.go -package=mocks galaxy/gamemaster/internal/ports RuntimeRecordStore
|
||||
|
||||
// RuntimeRecordStore stores runtime records and exposes the operations
|
||||
// used by the service layer (Stages 13+) and the workers (Stages 15-18).
|
||||
// Adapters must preserve domain semantics:
|
||||
//
|
||||
// - Get returns runtime.ErrNotFound when no record exists for gameID.
|
||||
// - Insert installs a fresh record and returns runtime.ErrConflict
|
||||
// when a row already exists.
|
||||
// - UpdateStatus applies one transition through a compare-and-swap
|
||||
// guard on the stored status and returns runtime.ErrConflict on a
|
||||
// stale CAS.
|
||||
// - UpdateScheduling mutates `next_generation_at`, `skip_next_tick`,
|
||||
// and `current_turn` together; the destination status is unaffected.
|
||||
// - UpdateImage rotates `current_image_ref` and
|
||||
// `current_engine_version` under a compare-and-swap guard on the
|
||||
// stored status and returns runtime.ErrConflict on a stale CAS.
|
||||
// - UpdateEngineHealth rotates the `engine_health` column without
|
||||
// touching status. The call applies from any status (including
|
||||
// stopped and finished) so late-arriving health observations still
|
||||
// bookkeep correctly. Returns runtime.ErrNotFound when no row
|
||||
// matches.
|
||||
// - Delete removes the record identified by gameID. The call is
|
||||
// idempotent: it returns nil even when no row matches.
|
||||
// - ListDueRunning returns every running record with
|
||||
// `next_generation_at <= now`.
|
||||
// - ListByStatus returns every record currently indexed under status.
|
||||
// - List returns every record ordered by `created_at` descending. Used
|
||||
// by the `internalListRuntimes` REST handler when no status filter
|
||||
// is supplied.
|
||||
type RuntimeRecordStore interface {
|
||||
// Get returns the record identified by gameID. It returns
|
||||
// runtime.ErrNotFound when no record exists.
|
||||
Get(ctx context.Context, gameID string) (runtime.RuntimeRecord, error)
|
||||
|
||||
// Insert installs record into the store. It returns
|
||||
// runtime.ErrConflict when a row already exists for record.GameID.
|
||||
Insert(ctx context.Context, record runtime.RuntimeRecord) error
|
||||
|
||||
// UpdateStatus applies one status transition in a compare-and-swap
|
||||
// fashion. The adapter must first call runtime.Transition to reject
|
||||
// invalid pairs without touching the store, then verify that the
|
||||
// stored status equals input.ExpectedFrom. Optional fields on the
|
||||
// input (CurrentImageRef, CurrentEngineVersion, EngineHealthSummary)
|
||||
// are persisted only when non-nil.
|
||||
UpdateStatus(ctx context.Context, input UpdateStatusInput) error
|
||||
|
||||
// UpdateScheduling mutates the scheduling columns
|
||||
// (`next_generation_at`, `skip_next_tick`, `current_turn`) of the
|
||||
// record identified by input.GameID. The store does not validate
|
||||
// the runtime status; callers issue UpdateScheduling alongside an
|
||||
// UpdateStatus when the destination status changes.
|
||||
UpdateScheduling(ctx context.Context, input UpdateSchedulingInput) error
|
||||
|
||||
// UpdateImage rotates `current_image_ref` and
|
||||
// `current_engine_version` of the record identified by
|
||||
// input.GameID under a compare-and-swap guard on the stored status.
|
||||
// The destination status is unchanged. Used by the admin patch
|
||||
// flow (Stage 17) where the runtime stays `running` while the
|
||||
// engine container is recreated by Runtime Manager with a new
|
||||
// image. Returns runtime.ErrNotFound when no row matches and
|
||||
// runtime.ErrConflict when the stored status differs from
|
||||
// input.ExpectedStatus.
|
||||
UpdateImage(ctx context.Context, input UpdateImageInput) error
|
||||
|
||||
// UpdateEngineHealth rotates the `engine_health` column of the
|
||||
// record identified by input.GameID without touching status. Used
|
||||
// by the runtime:health_events consumer (Stage 18) when an
|
||||
// observation should refresh the summary regardless of the current
|
||||
// runtime status (including stopped and finished, so late-arriving
|
||||
// events still bookkeep correctly). Returns runtime.ErrNotFound
|
||||
// when no row matches.
|
||||
UpdateEngineHealth(ctx context.Context, input UpdateEngineHealthInput) error
|
||||
|
||||
// Delete removes the record identified by gameID. The call is
|
||||
// idempotent: it returns nil even when no row matches. Used by the
|
||||
// register-runtime rollback path (Stage 13) when the engine
|
||||
// /admin/init call or any later setup step fails after the row has
|
||||
// been installed with status=starting.
|
||||
Delete(ctx context.Context, gameID string) error
|
||||
|
||||
// ListDueRunning returns every record whose status is `running`
|
||||
// and whose `next_generation_at <= now`. The order is
|
||||
// adapter-defined; callers may reorder as needed.
|
||||
ListDueRunning(ctx context.Context, now time.Time) ([]runtime.RuntimeRecord, error)
|
||||
|
||||
// ListByStatus returns every record currently indexed under status.
|
||||
// The order is adapter-defined; callers may reorder as needed.
|
||||
ListByStatus(ctx context.Context, status runtime.Status) ([]runtime.RuntimeRecord, error)
|
||||
|
||||
// List returns every record in the store, ordered by `created_at`
|
||||
// descending. Used by the `internalListRuntimes` REST handler when no
|
||||
// status filter is supplied.
|
||||
List(ctx context.Context) ([]runtime.RuntimeRecord, error)
|
||||
}
|
||||
|
||||
// UpdateStatusInput stores the arguments required to apply one status
|
||||
// transition through a RuntimeRecordStore. The optional fields are
|
||||
// pointers so the adapter can distinguish «leave alone» from «write
|
||||
// the zero value».
|
||||
type UpdateStatusInput struct {
|
||||
// GameID identifies the record to mutate.
|
||||
GameID string
|
||||
|
||||
// ExpectedFrom stores the status the caller believes the record
|
||||
// currently has. A mismatch results in runtime.ErrConflict.
|
||||
ExpectedFrom runtime.Status
|
||||
|
||||
// To stores the destination status.
|
||||
To runtime.Status
|
||||
|
||||
// Now stores the wall-clock used to derive the lifecycle timestamps
|
||||
// (started_at, stopped_at, finished_at, updated_at) according to
|
||||
// To.
|
||||
Now time.Time
|
||||
|
||||
// EngineHealthSummary is the new value of the `engine_health`
|
||||
// column. Nil leaves the column unchanged.
|
||||
EngineHealthSummary *string
|
||||
|
||||
// CurrentImageRef is the new value of the `current_image_ref`
|
||||
// column. Nil leaves the column unchanged. Used by the patch flow
|
||||
// (Stage 17) when the image reference rotates together with the
|
||||
// status update.
|
||||
CurrentImageRef *string
|
||||
|
||||
// CurrentEngineVersion is the new value of the
|
||||
// `current_engine_version` column. Nil leaves the column unchanged.
|
||||
// Used by the patch flow when the engine version rotates together
|
||||
// with the status update.
|
||||
CurrentEngineVersion *string
|
||||
}
|
||||
|
||||
// Validate reports whether input contains a structurally valid status
|
||||
// transition request. Adapters call Validate before touching the store.
|
||||
func (input UpdateStatusInput) Validate() error {
|
||||
if strings.TrimSpace(input.GameID) == "" {
|
||||
return fmt.Errorf("update runtime status: game id must not be empty")
|
||||
}
|
||||
if !input.ExpectedFrom.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update runtime status: expected from status %q is unsupported",
|
||||
input.ExpectedFrom,
|
||||
)
|
||||
}
|
||||
if !input.To.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update runtime status: to status %q is unsupported",
|
||||
input.To,
|
||||
)
|
||||
}
|
||||
if err := runtime.Transition(input.ExpectedFrom, input.To); err != nil {
|
||||
return fmt.Errorf("update runtime status: %w", err)
|
||||
}
|
||||
if input.Now.IsZero() {
|
||||
return fmt.Errorf("update runtime status: now must not be zero")
|
||||
}
|
||||
if input.CurrentImageRef != nil && strings.TrimSpace(*input.CurrentImageRef) == "" {
|
||||
return fmt.Errorf(
|
||||
"update runtime status: current image ref must not be empty when set",
|
||||
)
|
||||
}
|
||||
if input.CurrentEngineVersion != nil && strings.TrimSpace(*input.CurrentEngineVersion) == "" {
|
||||
return fmt.Errorf(
|
||||
"update runtime status: current engine version must not be empty when set",
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateSchedulingInput stores the arguments required to mutate the
|
||||
// scheduling columns of one runtime record. The status enum is
|
||||
// deliberately absent: scheduling and status updates are independent
|
||||
// operations and the service layer composes them when both must change.
|
||||
type UpdateSchedulingInput struct {
|
||||
// GameID identifies the record to mutate.
|
||||
GameID string
|
||||
|
||||
// NextGenerationAt is the new value of the column. Nil writes SQL
|
||||
// NULL (used to clear the tick when the runtime leaves running).
|
||||
NextGenerationAt *time.Time
|
||||
|
||||
// SkipNextTick is the new value of the column. The store overwrites
|
||||
// the column unconditionally.
|
||||
SkipNextTick bool
|
||||
|
||||
// CurrentTurn is the new value of the column. Must be non-negative.
|
||||
CurrentTurn int
|
||||
|
||||
// Now stores the wall-clock used to refresh `updated_at`.
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input contains structurally valid scheduling
|
||||
// arguments. Adapters call Validate before touching the store.
|
||||
func (input UpdateSchedulingInput) Validate() error {
|
||||
if strings.TrimSpace(input.GameID) == "" {
|
||||
return fmt.Errorf("update runtime scheduling: game id must not be empty")
|
||||
}
|
||||
if input.CurrentTurn < 0 {
|
||||
return fmt.Errorf("update runtime scheduling: current turn must not be negative")
|
||||
}
|
||||
if input.NextGenerationAt != nil && input.NextGenerationAt.IsZero() {
|
||||
return fmt.Errorf(
|
||||
"update runtime scheduling: next generation at must not be zero when set",
|
||||
)
|
||||
}
|
||||
if input.Now.IsZero() {
|
||||
return fmt.Errorf("update runtime scheduling: now must not be zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateImageInput stores the arguments required to rotate the engine
|
||||
// image reference and version of one runtime record without changing
|
||||
// its status. The store applies a compare-and-swap guard on
|
||||
// `(game_id, status)` so callers can reject the update if the runtime
|
||||
// has drifted out of the expected status.
|
||||
type UpdateImageInput struct {
|
||||
// GameID identifies the record to mutate.
|
||||
GameID string
|
||||
|
||||
// ExpectedStatus stores the status the caller believes the record
|
||||
// currently has. A mismatch results in runtime.ErrConflict.
|
||||
ExpectedStatus runtime.Status
|
||||
|
||||
// CurrentImageRef stores the new value of the
|
||||
// `current_image_ref` column. Must not be empty.
|
||||
CurrentImageRef string
|
||||
|
||||
// CurrentEngineVersion stores the new value of the
|
||||
// `current_engine_version` column. Must not be empty.
|
||||
CurrentEngineVersion string
|
||||
|
||||
// Now stores the wall-clock used to refresh `updated_at`.
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input contains structurally valid image
|
||||
// rotation arguments. Adapters call Validate before touching the store.
|
||||
func (input UpdateImageInput) Validate() error {
|
||||
if strings.TrimSpace(input.GameID) == "" {
|
||||
return fmt.Errorf("update runtime image: game id must not be empty")
|
||||
}
|
||||
if !input.ExpectedStatus.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update runtime image: expected status %q is unsupported",
|
||||
input.ExpectedStatus,
|
||||
)
|
||||
}
|
||||
if strings.TrimSpace(input.CurrentImageRef) == "" {
|
||||
return fmt.Errorf("update runtime image: current image ref must not be empty")
|
||||
}
|
||||
if strings.TrimSpace(input.CurrentEngineVersion) == "" {
|
||||
return fmt.Errorf("update runtime image: current engine version must not be empty")
|
||||
}
|
||||
if input.Now.IsZero() {
|
||||
return fmt.Errorf("update runtime image: now must not be zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEngineHealthInput stores the arguments required to rotate the
|
||||
// `engine_health` column of one runtime record without touching its
|
||||
// status. The store performs no compare-and-swap so callers can apply
|
||||
// the update from any runtime status (including stopped and finished)
|
||||
// to keep the summary current for late-arriving runtime:health_events.
|
||||
type UpdateEngineHealthInput struct {
|
||||
// GameID identifies the record to mutate.
|
||||
GameID string
|
||||
|
||||
// EngineHealthSummary stores the new value of the `engine_health`
|
||||
// column. The summary is a free-form short string drawn from the
|
||||
// vocabulary documented in
|
||||
// `gamemaster/README.md §Persistence Layout` and produced by the
|
||||
// Stage 18 consumer.
|
||||
EngineHealthSummary string
|
||||
|
||||
// Now stores the wall-clock used to refresh `updated_at`.
|
||||
Now time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input carries structurally valid arguments
|
||||
// for an engine-health update. Adapters call Validate before touching
|
||||
// the store.
|
||||
func (input UpdateEngineHealthInput) Validate() error {
|
||||
if strings.TrimSpace(input.GameID) == "" {
|
||||
return fmt.Errorf("update runtime engine health: game id must not be empty")
|
||||
}
|
||||
if input.Now.IsZero() {
|
||||
return fmt.Errorf("update runtime engine health: now must not be zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user