139 lines
4.7 KiB
Go
139 lines
4.7 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"galaxy/backend/internal/config"
|
|
"galaxy/backend/internal/dockerclient"
|
|
"galaxy/backend/internal/engineclient"
|
|
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// LobbyConsumer is the inbound surface the runtime uses to publish
|
|
// snapshots and adoption / removal events back into lobby. The
|
|
// canonical implementation is `*lobby.Service`; tests substitute a
|
|
// hand-rolled fake that records the calls.
|
|
//
|
|
// The interface is intentionally narrow: runtime only forwards
|
|
// data-plane events. Lobby owns every status transition that follows
|
|
// from the snapshot.
|
|
type LobbyConsumer interface {
|
|
// OnRuntimeSnapshot is invoked synchronously after every successful
|
|
// engine read or health-probe transition. Lobby maps the snapshot
|
|
// into its `games.runtime_snapshot` projection and may transition
|
|
// the game's lifecycle status.
|
|
OnRuntimeSnapshot(ctx context.Context, gameID uuid.UUID, snapshot LobbySnapshot) error
|
|
|
|
// OnRuntimeJobResult is invoked by the reconciler when a labelled
|
|
// container that lobby believes is alive has disappeared. Lobby
|
|
// reacts by cancelling the game (the engine container is gone).
|
|
OnRuntimeJobResult(ctx context.Context, gameID uuid.UUID, result JobResult) error
|
|
}
|
|
|
|
// LobbySnapshot is the runtime → lobby DTO. It is the runtime's view
|
|
// of the engine status response, plus the per-player observations
|
|
// lobby needs for capable-finish promotion.
|
|
//
|
|
// The structure intentionally mirrors `lobby.RuntimeSnapshot` in
|
|
// shape; runtime keeps its own version so the two packages do not
|
|
// import each other directly. The cmd/backend wiring layer adapts
|
|
// between them.
|
|
type LobbySnapshot struct {
|
|
CurrentTurn int32
|
|
RuntimeStatus string
|
|
EngineHealth string
|
|
ObservedAt time.Time
|
|
PlayerStats []LobbyPlayerStats
|
|
}
|
|
|
|
// LobbyPlayerStats is the per-player observation read from a runtime
|
|
// snapshot. `MaxPlanets` / `MaxPopulation` are the per-snapshot
|
|
// running maxima; lobby aggregates across the game lifetime.
|
|
type LobbyPlayerStats struct {
|
|
UserID uuid.UUID
|
|
InitialPlanets int32
|
|
InitialPopulation int32
|
|
CurrentPlanets int32
|
|
CurrentPopulation int32
|
|
MaxPlanets int32
|
|
MaxPopulation int32
|
|
}
|
|
|
|
// JobResult is the outcome envelope passed to
|
|
// `LobbyConsumer.OnRuntimeJobResult`. The reconciler produces it on
|
|
// adoption / removal events; future job paths (start, stop, restart)
|
|
// may reuse the same envelope.
|
|
type JobResult struct {
|
|
Op string
|
|
Status string
|
|
Message string
|
|
}
|
|
|
|
// NotificationPublisher is the outbound surface runtime uses to emit
|
|
// admin-channel notifications enumerated under `runtime.*` in
|
|
// `backend/README.md` §10. The real implementation lives in
|
|
// `backend/internal/notification` ; until then
|
|
// `NewNoopNotificationPublisher` ships a logger-only stub so the
|
|
// runtime path stays callable end-to-end during tests.
|
|
//
|
|
// Kind must be one of `runtime.image_pull_failed`,
|
|
// `runtime.container_start_failed`, or `runtime.start_config_invalid`.
|
|
// Payload carries the kind-specific fields documented in the catalog.
|
|
// The IdempotencyKey is supplied by the caller and feeds the
|
|
// notification UNIQUE(kind, idempotency_key) constraint.
|
|
type NotificationPublisher interface {
|
|
PublishRuntimeEvent(ctx context.Context, kind, idempotencyKey string, payload map[string]any) error
|
|
}
|
|
|
|
// NewNoopNotificationPublisher returns a NotificationPublisher that
|
|
// logs every event at info level and returns nil. The implementation swaps in
|
|
// the real `*notification.Service` adapter.
|
|
func NewNoopNotificationPublisher(logger *zap.Logger) NotificationPublisher {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
return &noopNotificationPublisher{logger: logger.Named("runtime.notify.noop")}
|
|
}
|
|
|
|
type noopNotificationPublisher struct {
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func (p *noopNotificationPublisher) PublishRuntimeEvent(_ context.Context, kind, idempotencyKey string, payload map[string]any) error {
|
|
p.logger.Info("runtime event (noop publisher)",
|
|
zap.String("kind", kind),
|
|
zap.String("idempotency_key", idempotencyKey),
|
|
zap.Int("payload_keys", len(payload)),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// Deps aggregates every collaborator the runtime Service depends on.
|
|
// Constructing the Service through Deps (rather than positional args)
|
|
// keeps the wiring patches small as new dependencies are added.
|
|
type Deps struct {
|
|
Store *Store
|
|
Cache *Cache
|
|
EngineVersions *EngineVersionService
|
|
|
|
Docker dockerclient.Client
|
|
Engine *engineclient.Client
|
|
Lobby LobbyConsumer
|
|
Notification NotificationPublisher
|
|
|
|
// DockerNetwork is the user-defined Docker network name engine
|
|
// containers attach to. Wired from `cfg.Docker.Network`.
|
|
DockerNetwork string
|
|
|
|
// HostStateRoot is the host-side directory that holds per-game
|
|
// state subdirectories. Wired from `cfg.Game.StateRoot`.
|
|
HostStateRoot string
|
|
|
|
Config config.RuntimeConfig
|
|
Logger *zap.Logger
|
|
Now func() time.Time
|
|
}
|