feat: runtime manager
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user