// Package ports defines the stable interfaces that connect Runtime // Manager use cases to external state and external services. package ports import ( "context" "fmt" "strings" "time" "galaxy/rtmanager/internal/domain/runtime" ) // 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. // - Upsert installs a record verbatim; the caller is responsible for // domain validation through runtime.RuntimeRecord.Validate. // - UpdateStatus applies one transition through a compare-and-swap // guard on (status, current_container_id) and returns // runtime.ErrConflict on a stale CAS. // - List returns every record currently stored, regardless of status. // - ListByStatus returns every record currently indexed under status. 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) // Upsert inserts record when no row exists for record.GameID and // otherwise overwrites every column verbatim. The start service uses // Upsert to install fresh records on start, the inner start of // restart and patch, and the reconcile_adopt path. Upsert(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, and (when // input.ExpectedContainerID is non-empty) that the stored // current_container_id equals it. The adapter derives stopped_at / // removed_at and updates last_op_at from input.Now per the // destination status. UpdateStatus(ctx context.Context, input UpdateStatusInput) error // List returns every runtime record currently stored. Used by the // internal REST list endpoint; the v1 working set is bounded by the // games tracked by Lobby and is small enough to return in one // response (pagination is not supported). The order is // adapter-defined; callers may reorder as needed. List(ctx context.Context) ([]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) } // UpdateStatusInput stores the arguments required to apply one status // transition through a RuntimeRecordStore. The adapter is responsible // for translating the destination status into the matching column // updates (stopped_at / removed_at / current_container_id NULLing) and // for the CAS guard. 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 // ExpectedContainerID is an optional CAS guard. When non-empty, the // adapter rejects the update with runtime.ErrConflict if the stored // current_container_id does not equal it. Used by stop / cleanup / // reconcile to protect against concurrent restart races. Empty // disables the container-id CAS while keeping the status CAS. ExpectedContainerID string // To stores the destination status. To runtime.Status // Now stores the wall-clock used to derive stopped_at / removed_at // and last_op_at depending on To. Now time.Time } // 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") } return nil }