Files
galaxy-game/rtmanager/internal/ports/runtimerecordstore.go
T
2026-04-28 20:39:18 +02:00

113 lines
4.5 KiB
Go

// 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
}