feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+189
View File
@@ -0,0 +1,189 @@
package runtime
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"time"
"galaxy/util"
)
// EngineVersionService implements the engine-version registry CRUD
// surface consumed by the admin endpoints under
// `/api/v1/admin/engine-versions/*`. Mutations are write-through: a
// successful Postgres write is followed by a cache update so warm
// reads observe the new state immediately.
type EngineVersionService struct {
store *Store
cache *Cache
now func() time.Time
}
// NewEngineVersionService constructs the service. now defaults to
// time.Now when nil.
func NewEngineVersionService(store *Store, cache *Cache, now func() time.Time) *EngineVersionService {
if now == nil {
now = time.Now
}
return &EngineVersionService{store: store, cache: cache, now: now}
}
// List returns every engine_versions row ordered by created_at DESC.
// Cache-first when warm; falls back to a Postgres read otherwise.
func (s *EngineVersionService) List(ctx context.Context) ([]EngineVersion, error) {
if s.cache != nil && s.cache.Ready() {
out := s.cache.ListEngineVersions()
sort.SliceStable(out, func(i, j int) bool {
if !out[i].CreatedAt.Equal(out[j].CreatedAt) {
return out[i].CreatedAt.After(out[j].CreatedAt)
}
return out[i].Version > out[j].Version
})
return out, nil
}
return s.store.ListEngineVersions(ctx)
}
// Get returns the row for version. Returns ErrNotFound on miss.
func (s *EngineVersionService) Get(ctx context.Context, version string) (EngineVersion, error) {
version = strings.TrimSpace(version)
if version == "" {
return EngineVersion{}, fmt.Errorf("%w: version must not be empty", ErrInvalidInput)
}
if s.cache != nil {
if v, ok := s.cache.GetEngineVersion(version); ok {
return v, nil
}
}
v, err := s.store.GetEngineVersion(ctx, version)
if err != nil {
return EngineVersion{}, err
}
if s.cache != nil {
s.cache.PutEngineVersion(v)
}
return v, nil
}
// RegisterInput is the parameter struct for Register.
type RegisterInput struct {
Version string
ImageRef string
Enabled *bool
}
// Validate normalises the request and rejects empty / malformed
// fields. Semver is enforced via `pkg/util.ParseSemver`.
func (in *RegisterInput) Validate() error {
in.Version = strings.TrimSpace(in.Version)
in.ImageRef = strings.TrimSpace(in.ImageRef)
if in.Version == "" {
return fmt.Errorf("%w: version must not be empty", ErrInvalidInput)
}
if _, err := util.ParseSemver(in.Version); err != nil {
return fmt.Errorf("%w: version %q is not a valid semver: %v", ErrInvalidInput, in.Version, err)
}
if in.ImageRef == "" {
return fmt.Errorf("%w: image_ref must not be empty", ErrInvalidInput)
}
return nil
}
// Register persists a fresh engine_versions row. Returns
// ErrEngineVersionTaken on duplicate version.
func (s *EngineVersionService) Register(ctx context.Context, in RegisterInput) (EngineVersion, error) {
if err := (&in).Validate(); err != nil {
return EngineVersion{}, err
}
enabled := true
if in.Enabled != nil {
enabled = *in.Enabled
}
now := s.now().UTC()
v, err := s.store.InsertEngineVersion(ctx, in.Version, in.ImageRef, enabled, now)
if err != nil {
return EngineVersion{}, err
}
if s.cache != nil {
s.cache.PutEngineVersion(v)
}
return v, nil
}
// UpdateInput is the parameter struct for Update. Nil pointers leave
// the corresponding column alone.
type UpdateInput struct {
ImageRef *string
Enabled *bool
}
// Update patches mutable fields on an existing row.
func (s *EngineVersionService) Update(ctx context.Context, version string, in UpdateInput) (EngineVersion, error) {
version = strings.TrimSpace(version)
if version == "" {
return EngineVersion{}, fmt.Errorf("%w: version must not be empty", ErrInvalidInput)
}
patch := engineVersionUpdate{Enabled: in.Enabled}
if in.ImageRef != nil {
trimmed := strings.TrimSpace(*in.ImageRef)
if trimmed == "" {
return EngineVersion{}, fmt.Errorf("%w: image_ref must not be empty", ErrInvalidInput)
}
patch.ImageRef = &trimmed
}
now := s.now().UTC()
v, err := s.store.UpdateEngineVersion(ctx, version, patch, now)
if err != nil {
return EngineVersion{}, err
}
if s.cache != nil {
s.cache.PutEngineVersion(v)
}
return v, nil
}
// Disable flips the enabled flag to false. Idempotent.
func (s *EngineVersionService) Disable(ctx context.Context, version string) (EngineVersion, error) {
disabled := false
return s.Update(ctx, version, UpdateInput{Enabled: &disabled})
}
// Resolve returns the row for version, rejecting disabled rows with
// ErrEngineVersionDisabled. Used by `Service.StartGame` /
// `AdminPatch` / `AdminRestart` before the docker pull.
func (s *EngineVersionService) Resolve(ctx context.Context, version string) (EngineVersion, error) {
v, err := s.Get(ctx, version)
if err != nil {
return EngineVersion{}, err
}
if !v.Enabled {
return EngineVersion{}, fmt.Errorf("%w: %s", ErrEngineVersionDisabled, v.Version)
}
return v, nil
}
// CheckPatchCompatible verifies the requested target version stays
// inside the same major+minor line as `currentVersion`. Returns
// ErrPatchSemverIncompatible otherwise.
func CheckPatchCompatible(currentVersion, targetVersion string) error {
current, err := util.ParseSemver(currentVersion)
if err != nil {
return fmt.Errorf("%w: current version %q: %v", ErrInvalidInput, currentVersion, err)
}
target, err := util.ParseSemver(targetVersion)
if err != nil {
return fmt.Errorf("%w: target version %q: %v", ErrInvalidInput, targetVersion, err)
}
if current.Major != target.Major || current.Minor != target.Minor {
return fmt.Errorf("%w: %s -> %s", ErrPatchSemverIncompatible, currentVersion, targetVersion)
}
return nil
}
// IsKnownEngineVersion is a small helper used by tests and handlers.
func IsKnownEngineVersion(err error) bool {
return errors.Is(err, ErrEngineVersionDisabled) || errors.Is(err, ErrPatchSemverIncompatible)
}