scale

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 30, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package scale is the opt-in, host-aware process auto-scaler (plan §8A). It scales container REPLICAS of one edge-fronted HTTP service — never VMs, never the whole project — and is conservative-by-construction: it REFUSES rather than queues, treats a refusal as an alertable signal, and on a small box collapses to a safe no-op (effective_max = 1).

This file is the candidacy gate. The decision core (controller hysteresis) and the load-bearing host-capacity guard are in controller.go / capacity.go. All three are pure so the safety properties are exhaustively testable; the watcher (watcher.go) supplies the inputs, applies the §0 gate + semaphore, and performs the scale.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Candidacy

func Candidacy(s ServiceSpec) (ok bool, reason string)

Candidacy reports whether a service may be auto-scaled, with the first failing reason. Default is NOT scalable: every condition must hold. A stateful service is rejected with a clear reason — it is a config-file/cert-binding app (§7.4), not a scaling candidate.

func MaxReplicas

func MaxReplicas(in CapacityInput) (ceiling int, nearOOM bool, reason string)

MaxReplicas returns the hard replica ceiling this service may run RIGHT NOW. It is never below 1 (a service always keeps its base replica) and is capped by the policy max and BOTH resource budgets. nearOOM=true means the box is critically low on memory and scaling is a no-op (effective_max = 1) — a wanted scale-up above this is a refusal the caller must surface as scale_refused_no_capacity.

func StatefulImage

func StatefulImage(image string) bool

StatefulImage reports whether an image reference belongs to a known stateful / clustered family (C4). It strips the registry, tag, and digest, then matches the final repository path component against the denylist (so "ghcr.io/acme/postgres:16" and "postgres" both match, but "my-postgres-helper" does not).

Types

type Action

type Action string

Action is what the watcher should do with the decision.

const (
	ActNone    Action = "none"
	ActUp      Action = "up"
	ActDown    Action = "down"
	ActRefused Action = "refused" // wanted to scale up but the capacity ceiling blocked it
)

type Budget

type Budget struct {
	HostTotal  uint64 // total host resource
	HostFree   uint64 // measured-free right now
	Reserved   uint64 // everything NOT this service: control plane + edge + safety floor + OTHER apps' desired replicas
	FreeFloor  uint64 // keep at least this much free (applied to the measured budget)
	PerReplica uint64 // this service's per-replica reservation (required, non-zero)
	Current    int    // this service's current replica count
}

Budget is one resource's accounting (memory in bytes, or CPU in milli-units).

type CapacityInput

type CapacityInput struct {
	Mem Budget
	CPU Budget

	PolicyMax          int    // operator's configured max_replicas
	PerReplicaMemFloor uint64 // an implausibly small per-replica mem reservation is rejected
	NearOOMFreeBytes   uint64 // host mem free below this → effective_max collapses to 1
}

CapacityInput is everything MaxReplicas needs for one service this tick.

type Config

type Config struct {
	Store        *Store
	Alerts       *alertstore.Store // nil → refusals are logged only
	Snap         func() *monitor.Snapshot
	Sem          *dockerexec.Semaphore
	Scaler       Scaler
	Edge         EdgeReconciler // optional
	Reserves     Reserves
	Log          *slog.Logger
	Interval     time.Duration
	WritePlaneOK bool
	HostCPUMilli uint64                                        // total host CPU (milli); 0 disables the CPU budget
	IsCandidate  func(app, service string) (ServiceSpec, bool) // C1–C6 from compose; nil → trust the policy opt-in
	Now          func() int64
}

Config configures the auto-scaling Watcher.

type Decision

type Decision struct {
	Target int
	Action Action
	Reason string
	Next   State
}

Decision is the pure outcome; the watcher persists Next and, on Up/Down, performs the scale (+ edge-pool reconcile). On Refused it raises scale_refused_no_capacity.

func Decide

func Decide(st State, m Metrics, p Policy, ceiling int, now int64) Decision

Decide steps the controller for one service. ceiling is the host-capacity guard's hard cap for this tick (from MaxReplicas).

type EdgeReconciler

type EdgeReconciler interface {
	ReconcilePool(ctx context.Context, app, service string, replicas int) error
}

EdgeReconciler updates the edge replica pool for a service after a count change (discover live replicas → validated pool → reload). May be nil (then the route's single upstream DNS-round-robins across replicas).

type Key

type Key struct{ App, Service string }

Key identifies one scaled service.

type Metrics

type Metrics struct {
	CPUMeanPct float64
	MemMaxPct  float64
	AllHealthy bool
}

Metrics is the per-service signal: per-replica CPU MEAN and mem MAX aggregated across the running replicas (plan §8A), plus whether every replica is healthy.

type Policy

type Policy struct {
	Min, Max         int
	UpCPUPct         float64
	UpMemPct         float64
	DownCPUPct       float64
	DownMemPct       float64
	BreachForSecs    int64
	CooldownUpSecs   int64
	CooldownDownSecs int64
}

Policy is the operator's scaling policy for one service.

func (Policy) Valid

func (p Policy) Valid() (bool, string)

Valid checks the policy invariants config validation must enforce: a sane replica range, the ≥20-pt dead band on BOTH signals, and up-eager/down-lazy cooldowns.

type PolicyRow

type PolicyRow struct {
	Policy
	Enabled       bool
	PerReplicaMem uint64
	PerReplicaCPU uint64
}

PolicyRow is a stored policy plus its per-replica reservations + enabled flag.

type Reserves

type Reserves struct {
	MemReserveBytes    uint64 // control plane + edge + safety floor (memory)
	CPUReserveMilli    uint64 // control plane + edge (cpu)
	MemFreeFloor       uint64 // keep at least this much memory free (measured budget)
	CPUFreeFloor       uint64
	NearOOMFreeBytes   uint64
	PerReplicaMemFloor uint64
}

Reserves are the host headroom the capacity guard subtracts before funding this app's replicas (control plane + edge slice + a safety floor), plus the near-OOM and per-replica-floor guards.

type Scaler

type Scaler interface {
	Scale(ctx context.Context, app, service string, replicas int) error
}

Scaler performs the actual replica change for a service (static-argv `docker compose up -d --no-deps --no-recreate --scale <svc>=<n>`). The watcher calls it only after the §0 gate + a non-blocking semaphore acquire, holding the one-docker-child semaphore.

type ServiceSpec

type ServiceSpec struct {
	Name                string
	EdgeUpstream        bool // C1: an edge HTTP upstream with a known internal port
	L4Upstream          bool // C1 (alt): a managed L4 (TCP/UDP) upstream — fronted by an edge l4_route, replicas internal-only
	FixedHostPort       bool // C2 (disqualifies): publishes a fixed host port the LB does NOT own
	RWVolume            bool // C3 (disqualifies): has an exclusive read-write volume
	Stateful            bool // C4 (disqualifies): a DB/broker/coordination store
	IdentityPlaceholder bool // C5 (disqualifies): a deploy-time identity (node cookie/name/seed/host-bound port)
	StatelessContract   bool // C6: honors the stateless restart contract (operator attests)
	OptedIn             bool // C7: the operator explicitly enabled scaling for this service
}

ServiceSpec is the deploy-time view of one service, derived from its compose definition + the managed edge routes. Candidacy is re-evaluated on every deploy / config change; a service that GAINS a host port or RW volume loses candidacy and is scaled back to 1.

type State

type State struct {
	Replicas    int
	BreachSince int64 // unix sec; when the current up-breach started (0 = not breaching)
	LastChange  int64 // unix sec; last scale action (for the cooldowns)
}

State is the persisted controller state for one service. Replicas is the DESIRED count the controller is driving toward (the watcher reconciles observed→desired).

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store persists scaling policies (operator opt-in + thresholds) and controller state (desired replicas + hysteresis timers, recovered on restart).

func NewStore

func NewStore(db *store.DB) *Store

NewStore builds a Store.

func (*Store) DeleteApp

func (s *Store) DeleteApp(ctx context.Context, app string) error

SavePolicy validates + upserts a policy. A policy must pass Policy.Valid() and carry non-zero per-replica reservations before it can be enabled. DeleteApp removes ALL scaling policies and controller state for every service of an app. Used by the app-delete teardown.

func (*Store) EnabledPolicies

func (s *Store) EnabledPolicies() (map[Key]PolicyRow, error)

EnabledPolicies returns every enabled policy keyed by (app,service).

func (*Store) LoadStates

func (s *Store) LoadStates() (map[Key]State, error)

LoadStates returns all persisted controller states.

func (*Store) PolicyFor

func (s *Store) PolicyFor(k Key) (PolicyRow, bool, error)

PolicyFor returns the policy for one service (enabled flag included), or ok=false.

func (*Store) SavePolicy

func (s *Store) SavePolicy(ctx context.Context, k Key, pr PolicyRow) error

func (*Store) SaveState

func (s *Store) SaveState(ctx context.Context, k Key, st State, now int64) error

SaveState upserts one controller state.

type Watcher

type Watcher struct {
	// contains filtered or unexported fields
}

Watcher is the auto-scaling controller loop (plan §8A).

func New

func New(cfg Config) *Watcher

New builds a Watcher.

func (*Watcher) Run

func (w *Watcher) Run(ctx context.Context)

Run recovers state and ticks until ctx is cancelled.

func (*Watcher) Tick

func (w *Watcher) Tick(ctx context.Context)

Tick runs one control pass. Exported for tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL