feat: gamemaster
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
// Package operation defines the runtime-operation audit-log domain
|
||||
// types owned by Game Master.
|
||||
//
|
||||
// One OperationEntry maps to one row of the `operation_log` PostgreSQL
|
||||
// table (see
|
||||
// `galaxy/gamemaster/internal/adapters/postgres/migrations/00001_init.sql`).
|
||||
// The OpKind / OpSource / Outcome enums match the SQL CHECK constraints
|
||||
// verbatim and feed the telemetry counters declared in
|
||||
// `galaxy/gamemaster/README.md §Observability`.
|
||||
package operation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OpKind identifies the kind of operation Game Master performed.
|
||||
type OpKind string
|
||||
|
||||
const (
|
||||
// OpKindRegisterRuntime records a register-runtime operation
|
||||
// (engine init plus first transition to running).
|
||||
OpKindRegisterRuntime OpKind = "register_runtime"
|
||||
|
||||
// OpKindTurnGeneration records a turn-generation operation
|
||||
// (scheduler ticker or admin force).
|
||||
OpKindTurnGeneration OpKind = "turn_generation"
|
||||
|
||||
// OpKindForceNextTurn records the admin force-next-turn driver
|
||||
// (separate from the turn-generation entry it produces, so audit
|
||||
// callers can tell scheduler ticks from manual ones).
|
||||
OpKindForceNextTurn OpKind = "force_next_turn"
|
||||
|
||||
// OpKindBanish records a /admin/race/banish call against the
|
||||
// engine container.
|
||||
OpKindBanish OpKind = "banish"
|
||||
|
||||
// OpKindStop records the admin stop driver (the underlying RTM
|
||||
// stop call is recorded in Runtime Manager's own operation log).
|
||||
OpKindStop OpKind = "stop"
|
||||
|
||||
// OpKindPatch records the admin patch driver.
|
||||
OpKindPatch OpKind = "patch"
|
||||
|
||||
// OpKindEngineVersionCreate records a registry CREATE.
|
||||
OpKindEngineVersionCreate OpKind = "engine_version_create"
|
||||
|
||||
// OpKindEngineVersionUpdate records a registry PATCH.
|
||||
OpKindEngineVersionUpdate OpKind = "engine_version_update"
|
||||
|
||||
// OpKindEngineVersionDeprecate records a registry DELETE / soft
|
||||
// deprecate.
|
||||
OpKindEngineVersionDeprecate OpKind = "engine_version_deprecate"
|
||||
|
||||
// OpKindEngineVersionDelete records a registry hard delete: the
|
||||
// row is removed from `engine_versions` after the service layer
|
||||
// confirms no non-finished runtime still references it.
|
||||
OpKindEngineVersionDelete OpKind = "engine_version_delete"
|
||||
)
|
||||
|
||||
// IsKnown reports whether kind belongs to the frozen op-kind vocabulary.
|
||||
func (kind OpKind) IsKnown() bool {
|
||||
switch kind {
|
||||
case OpKindRegisterRuntime,
|
||||
OpKindTurnGeneration,
|
||||
OpKindForceNextTurn,
|
||||
OpKindBanish,
|
||||
OpKindStop,
|
||||
OpKindPatch,
|
||||
OpKindEngineVersionCreate,
|
||||
OpKindEngineVersionUpdate,
|
||||
OpKindEngineVersionDeprecate,
|
||||
OpKindEngineVersionDelete:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// AllOpKinds returns the frozen list of every op-kind value. The slice
|
||||
// order is stable across calls.
|
||||
func AllOpKinds() []OpKind {
|
||||
return []OpKind{
|
||||
OpKindRegisterRuntime,
|
||||
OpKindTurnGeneration,
|
||||
OpKindForceNextTurn,
|
||||
OpKindBanish,
|
||||
OpKindStop,
|
||||
OpKindPatch,
|
||||
OpKindEngineVersionCreate,
|
||||
OpKindEngineVersionUpdate,
|
||||
OpKindEngineVersionDeprecate,
|
||||
OpKindEngineVersionDelete,
|
||||
}
|
||||
}
|
||||
|
||||
// OpSource identifies where one operation entered Game Master.
|
||||
type OpSource string
|
||||
|
||||
const (
|
||||
// OpSourceGatewayPlayer identifies entries triggered by a verified
|
||||
// player command, order, or report read forwarded through Edge
|
||||
// Gateway.
|
||||
OpSourceGatewayPlayer OpSource = "gateway_player"
|
||||
|
||||
// OpSourceLobbyInternal identifies entries triggered by Game Lobby
|
||||
// over the trusted internal REST surface (register-runtime,
|
||||
// memberships invalidate, banish, liveness).
|
||||
OpSourceLobbyInternal OpSource = "lobby_internal"
|
||||
|
||||
// OpSourceAdminRest identifies entries triggered by Admin Service
|
||||
// (or system administrators today). The default when the
|
||||
// `X-Galaxy-Caller` header is missing or unrecognised.
|
||||
OpSourceAdminRest OpSource = "admin_rest"
|
||||
)
|
||||
|
||||
// IsKnown reports whether source belongs to the frozen op-source
|
||||
// vocabulary.
|
||||
func (source OpSource) IsKnown() bool {
|
||||
switch source {
|
||||
case OpSourceGatewayPlayer,
|
||||
OpSourceLobbyInternal,
|
||||
OpSourceAdminRest:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// AllOpSources returns the frozen list of every op-source value. The
|
||||
// slice order is stable across calls.
|
||||
func AllOpSources() []OpSource {
|
||||
return []OpSource{
|
||||
OpSourceGatewayPlayer,
|
||||
OpSourceLobbyInternal,
|
||||
OpSourceAdminRest,
|
||||
}
|
||||
}
|
||||
|
||||
// Outcome reports the high-level outcome of one operation.
|
||||
type Outcome string
|
||||
|
||||
const (
|
||||
// OutcomeSuccess reports that the operation completed without
|
||||
// surfacing an error.
|
||||
OutcomeSuccess Outcome = "success"
|
||||
|
||||
// OutcomeFailure reports that the operation surfaced a stable
|
||||
// error code recorded in OperationEntry.ErrorCode.
|
||||
OutcomeFailure Outcome = "failure"
|
||||
)
|
||||
|
||||
// IsKnown reports whether outcome belongs to the frozen outcome
|
||||
// vocabulary.
|
||||
func (outcome Outcome) IsKnown() bool {
|
||||
switch outcome {
|
||||
case OutcomeSuccess, OutcomeFailure:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// AllOutcomes returns the frozen list of every outcome value. The slice
|
||||
// order is stable across calls.
|
||||
func AllOutcomes() []Outcome {
|
||||
return []Outcome{OutcomeSuccess, OutcomeFailure}
|
||||
}
|
||||
|
||||
// OperationEntry stores one append-only audit row of the `operation_log`
|
||||
// table. ID is zero on records that have not been persisted yet; the
|
||||
// store assigns it from the table's bigserial column. FinishedAt is a
|
||||
// pointer because the column is nullable for in-flight rows even though
|
||||
// the service layer finalises the row in the same transaction.
|
||||
type OperationEntry struct {
|
||||
// ID identifies the persisted row. Zero before persistence.
|
||||
ID int64
|
||||
|
||||
// GameID identifies the platform game this operation acted on.
|
||||
GameID string
|
||||
|
||||
// OpKind classifies what the operation did.
|
||||
OpKind OpKind
|
||||
|
||||
// OpSource classifies how the operation entered Game Master.
|
||||
OpSource OpSource
|
||||
|
||||
// SourceRef stores an opaque per-source reference such as a request
|
||||
// id, a Redis Stream entry id, or an admin user id. Empty when the
|
||||
// source does not provide one.
|
||||
SourceRef string
|
||||
|
||||
// Outcome reports whether the operation succeeded or failed.
|
||||
Outcome Outcome
|
||||
|
||||
// ErrorCode stores the stable error code on failure. Empty on
|
||||
// success.
|
||||
ErrorCode string
|
||||
|
||||
// ErrorMessage stores the operator-readable detail on failure.
|
||||
// Empty on success.
|
||||
ErrorMessage string
|
||||
|
||||
// StartedAt stores the wall-clock at which the operation began.
|
||||
StartedAt time.Time
|
||||
|
||||
// FinishedAt stores the wall-clock at which the operation
|
||||
// finalised. Nil for in-flight rows.
|
||||
FinishedAt *time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether entry satisfies the operation-log invariants
|
||||
// implied by the SQL CHECK constraints and the README §Persistence
|
||||
// Layout listing.
|
||||
func (entry OperationEntry) Validate() error {
|
||||
if strings.TrimSpace(entry.GameID) == "" {
|
||||
return fmt.Errorf("game id must not be empty")
|
||||
}
|
||||
if !entry.OpKind.IsKnown() {
|
||||
return fmt.Errorf("op kind %q is unsupported", entry.OpKind)
|
||||
}
|
||||
if !entry.OpSource.IsKnown() {
|
||||
return fmt.Errorf("op source %q is unsupported", entry.OpSource)
|
||||
}
|
||||
if !entry.Outcome.IsKnown() {
|
||||
return fmt.Errorf("outcome %q is unsupported", entry.Outcome)
|
||||
}
|
||||
if entry.StartedAt.IsZero() {
|
||||
return fmt.Errorf("started at must not be zero")
|
||||
}
|
||||
if entry.FinishedAt != nil {
|
||||
if entry.FinishedAt.IsZero() {
|
||||
return fmt.Errorf("finished at must not be zero when present")
|
||||
}
|
||||
if entry.FinishedAt.Before(entry.StartedAt) {
|
||||
return fmt.Errorf("finished at must not be before started at")
|
||||
}
|
||||
}
|
||||
if entry.Outcome == OutcomeFailure && strings.TrimSpace(entry.ErrorCode) == "" {
|
||||
return fmt.Errorf("error code must not be empty for failure entries")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user