autoscaler

package
v0.7.9 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

pkg/autoscaler

The autoscaler package evaluates time-based and metric-based conditions on a periodic tick and adjusts an operatorbox's worker count, queue depth, and resync interval at runtime — without stopping or restarting goroutines.

One Autoscaler instance runs per operatorbox that declares autoscale: in its operatorBox: block. It owns a single goroutine for the lifetime of the operatorbox and restores the declared baseline on clean shutdown.

What lives here

File Role
autoscaler.go Autoscaler — evaluation loop; condition dispatch; apply/restore logic
autoscale_semaphore.go ResizableSemaphore — O(1) concurrency gate; resize without stopping goroutines
autoscale_metrics.go AutoMetrics — atomic runtime counters (queue depth, P95 latency, error rate)
autoscale_cross_metrics.go CrossMetricsRegistry — shared registry; enables cross.<crd>.metrics.* conditions
autoscale_worker_info.go WorkerInfo + BuildWorkerInfo — serialisable worker snapshot for the CRD endpoint

Developer documentation

Full step-by-step documentation is in docs/.

I want to… Go to
Understand the evaluation loop and override lifecycle 01 — Overview
Write anyOf: / when: conditions 02 — Conditions
Reference another CRD's metrics with cross.<crd>.metrics.* 03 — Cross-Metrics
Understand the WorkerInfo API response 04 — Worker Info

For the operatorBox.autoscale: YAML declaration, see docs/runtime-manual/concepts/operator-autoscaler.md.

Documentation

Overview

pkg/autoscaler/autoscale_cross_metrics.go

CrossMetricsRegistry — a shared registry of AutoMetrics instances, one per operatorbox. Enables autoscale conditions to reference another operatorbox's live metrics via the cross.metrics.* namespace.

Usage in Katalog:

# The database-backed-app operatorbox scales based on the database
# operatorbox's queue depth — if the DB is overwhelmed, slow down.
operatorBox:
  autoscale:
    conditions:
      when:
        - field: cross.managed-database.metrics.queueDepth
          greaterThan: "500"
    do:
      workers: 2   # back off while DB is under load

Resolution:

cross.<crd>.metrics.<field>
  → CrossMetricsRegistry.Get(<crd>).Get("metrics.<field>")

This is read-only. An operatorbox can observe another's metrics but cannot modify them. The same principle as cross-CRD CR state observation.

Thread safety: Register is called once at startup per operatorbox (under Kordinator init). Get is called on every autoscale tick (concurrent reads). sync.Map provides safe concurrent access with no lock contention on reads.

pkg/autoscaler/autoscale_metrics.go

AutoMetrics — live operatorbox metrics read directly from the runtime.

The autoscaler evaluates conditions against these metrics on every tick. All reads are atomic — no locks, no API calls, no informer lookups.

Metric fields are exposed under the "metrics.*" namespace in autoscale conditions and resolve through AutoMetrics.Get(field).

pkg/autoscaler/autoscale_semaphore.go

ResizableSemaphore — a weighted semaphore whose capacity can be changed at runtime.

The worker pool in Orkestra is gated by this semaphore rather than being a fixed goroutine count. All worker goroutines run continuously in a loop; the semaphore controls how many may enter the reconcile section simultaneously.

Increasing capacity: new goroutines can acquire immediately. Decreasing capacity: goroutines currently inside reconcile finish their current work. Goroutines blocked at Acquire wait until capacity is available. In-flight reconciles are never interrupted.

This is the correct primitive for operator autoscaling because:

  • Resize is O(1) — no goroutine creation or cancellation
  • In-flight reconciles complete cleanly after a scale-down
  • The semaphore is the single source of truth for concurrency

pkg/autoscaler/autoscale_worker_info.go

WorkerInfo — the complete picture of an operatorbox's worker state. Returned by the CRD handler endpoint and shown in the Control Center.

The Control Center shows:

Configured: 4      ← CRD declared workers (always the baseline)
Effective:  12     ← current semaphore capacity (may be overridden)
InFlight:   7      ← goroutines currently reconciling
Idle:       5      ← effective - inFlight
Max:        12     ← highest workers ever from do: block (pre-allocated)
Autoscaler: ✓ enabled, override active

This replaces the misleading "N pending workers" display that showed all pre-allocated goroutines as pending regardless of autoscale state.

pkg/autoscaler/autoscaler.go

Operatorbox autoscaler — evaluates conditions on a ticker and applies or restores worker/queue/resync overrides.

One Autoscaler is created per operatorbox that declares autoscale: in its operatorBox: block. It runs a single goroutine for the lifetime of the operatorbox and stops cleanly when the context is cancelled.

The autoscaler has no persistent state. A restart always begins from the declared CRD baseline. Overrides applied before a restart are lost — the next evaluation tick will re-apply them if conditions are still met.

Index

Constants

This section is empty.

Variables

View Source
var GlobalCrossMetricsRegistry = &CrossMetricsRegistry{}

GlobalCrossMetricsRegistry is the process-wide registry. Populated during Kordinator startup, read by autoscalers at runtime.

Functions

func IsMetricField

func IsMetricField(field string) bool

IsMetricField returns true when the field path refers to an autoscale metric. Used by the condition resolver to route metric lookups to AutoMetrics.Get.

func OldResolveCrossMetric added in v0.4.5

func OldResolveCrossMetric(registry *CrossMetricsRegistry, field string, source *orktypes.CrossSource) string

ResolveCrossMetric resolves a cross.metrics field path of the form:

cross.<crd>.metrics.<field>

Resolution order:

  1. GlobalCrossMetricsRegistry — zero hops, same-binary CRDs
  2. source.Endpoint HTTP call — one hop, cross-binary CRDs The endpoint must be the remote operator's /katalog/{crd} URL. The response is expected to carry a "metrics" object with the same field names as AutoMetrics.AsMap().

Returns "" when neither path finds a value.

func ResolveCrossMetric added in v0.1.9

func ResolveCrossMetric(
	registry *CrossMetricsRegistry,
	field string,
	source *orktypes.CrossSource,
) string

Types

type AutoMetrics

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

AutoMetrics holds live operatorbox runtime metrics for autoscale evaluation. All counters are atomic int64 values — safe for concurrent read/write from worker goroutines and the autoscaler loop.

func NewAutoMetrics

func NewAutoMetrics(sem *ResizableSemaphore) *AutoMetrics

NewAutoMetrics returns an initialised AutoMetrics for the given semaphore.

func (*AutoMetrics) AsMap

func (m *AutoMetrics) AsMap() map[string]interface{}

func (*AutoMetrics) ErrorRatePercent

func (m *AutoMetrics) ErrorRatePercent() float64

ErrorRatePercent returns the curent error rate i percentage

func (*AutoMetrics) Get

func (m *AutoMetrics) Get(field string) string

Get returns the string representation of a metric field for condition evaluation. Field names follow the metrics.* convention:

metrics.workersBusyPercent   → "73.5"
metrics.workersIdlePercent   → "26.5"
metrics.queueDepth           → "342"
metrics.reconcileDurationP95Ms → "47"
metrics.errorRatePercent     → "0.2"

Returns "" for unknown fields — evaluates as notExists in condition checks.

func (*AutoMetrics) RecordReconcile

func (m *AutoMetrics) RecordReconcile(duration time.Duration, failed bool)

RecordReconcile records one completed reconcile with its duration and outcome. Called at the end of every reconcile, success or failure.

func (*AutoMetrics) SetQueueDepth

func (m *AutoMetrics) SetQueueDepth(depth int64)

SetQueueDepth updates the current queue depth. Called by the workqueue wrapper on every enqueue and dequeue.

type AutoscaleTarget

type AutoscaleTarget interface {
	// ResizeWorkers adjusts the worker pool to the given concurrency.
	ResizeWorkers(n int)

	// SetQueueDepthLimit adjusts the maximum queue depth.
	SetQueueDepthLimit(n int)

	// SetResyncInterval adjusts how frequently all CRs are re-enqueued.
	SetResyncInterval(d time.Duration)
}

AutoscaleTarget is implemented by the operatorbox runtime components that the autoscaler controls. Kept minimal — autoscaler only calls these three methods, nothing else.

type Autoscaler

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

Autoscaler evaluates conditions and applies/restores overrides for one operatorbox.

func NewAutoscaler

func NewAutoscaler(
	crdKind string,
	spec *orktypes.AutoscaleSpec,
	baseline orktypes.AutoscaleBaseline,
	target AutoscaleTarget,
	metrics *AutoMetrics,
	crossDecls []orktypes.CrossCRDDeclaration,
) *Autoscaler

NewAutoscaler constructs an Autoscaler for one operatorbox. baseline is captured from the CRD's declared configuration before startup. crossDecls is the operatorBox.cross slice — used to resolve source fallback for autoscale conditions that reference cross.<crd>.metrics.* without an explicit source: block on the condition itself.

func (*Autoscaler) Run

func (a *Autoscaler) Run(ctx context.Context)

Run starts the autoscale evaluation loop. Blocks until ctx is cancelled. Intended to be called in a dedicated goroutine.

func (*Autoscaler) Snapshot added in v0.1.9

func (a *Autoscaler) Snapshot() *autoscalerStateSnapshot

Snapshot returns a point-in-time snapshot of the Autoscaler's state. Called by the CRD handler without holding any lock — reads atomic values.

type CrossMetricsRegistry added in v0.1.9

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

CrossMetricsRegistry holds AutoMetrics for all registered operatorboxes. Keyed by lowercase CRD crd name — the same key used in cross: declarations.

func (*CrossMetricsRegistry) Get added in v0.1.9

Get returns the AutoMetrics for the given crd, or nil if not registered. crd is normalized to lowercase — "ManagedDatabase" and "manageddatabase" are equivalent.

func (*CrossMetricsRegistry) Register added in v0.1.9

func (r *CrossMetricsRegistry) Register(crd string, metrics *AutoMetrics)

Register registers an operatorbox's AutoMetrics under its crd name. Called once per operatorbox during Kordinator startup. crd is normalized to lowercase for consistent lookup.

type ResizableSemaphore

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

ResizableSemaphore is a counting semaphore whose capacity can be changed at runtime without interrupting goroutines currently holding a token.

func NewResizableSemaphore

func NewResizableSemaphore(capacity int) *ResizableSemaphore

NewResizableSemaphore returns a semaphore with the given initial capacity.

func (*ResizableSemaphore) Acquire

func (s *ResizableSemaphore) Acquire(ctx context.Context) error

Acquire acquires one token, blocking until one is available or ctx is done. Returns ctx.Err() if the context is cancelled while waiting.

func (*ResizableSemaphore) BusyPercent

func (s *ResizableSemaphore) BusyPercent() float64

BusyPercent returns the percentage of capacity currently in use.

func (*ResizableSemaphore) Capacity

func (s *ResizableSemaphore) Capacity() int

Capacity returns the current capacity.

func (*ResizableSemaphore) Current

func (s *ResizableSemaphore) Current() int

Current returns the current number of tokens.

func (*ResizableSemaphore) IdlePercent

func (s *ResizableSemaphore) IdlePercent() float64

IdlePercent returns the percentage of capacity currently available.

func (*ResizableSemaphore) InFlight

func (s *ResizableSemaphore) InFlight() int

InFlight returns the number of tokens currently held (goroutines inside reconcile).

func (*ResizableSemaphore) Release

func (s *ResizableSemaphore) Release()

Release releases one token and notifies a waiter if any are queued.

func (*ResizableSemaphore) Resize

func (s *ResizableSemaphore) Resize(newCap int)

Resize changes the semaphore capacity.

Scale up (newCap > old): immediately notifies waiting goroutines up to the new capacity. Each notification allows one waiter to proceed.

Scale down (newCap < old): the new capacity takes effect immediately for new Acquire calls. Goroutines already holding tokens complete their work normally — they are not interrupted. The effective concurrency converges to newCap as holders release.

Resize is safe to call concurrently with Acquire and Release.

type WorkerInfo added in v0.1.9

type WorkerInfo struct {
	// Configured is the baseline workers declared in the CRD entry.
	// This is what the operatorbox returns to after an autoscale override ends.
	Configured int `json:"configured"`

	// Effective is the current semaphore capacity — the real concurrency limit.
	// Equals Configured when no autoscale override is active.
	// Equals the override value when autoscale is active.
	Effective int `json:"effective"`

	// InFlight is the number of goroutines currently inside a reconcile call.
	// InFlight <= Effective always.
	InFlight int `json:"inFlight"`

	// Idle is how many effective worker slots are available right now.
	// Idle = Effective - InFlight.
	Idle int `json:"idle"`

	// Max is the maximum workers the autoscaler can scale to (from do.workers).
	// Zero when autoscaler is not declared.
	// This is pre-allocated — the goroutine pool is sized to Max at startup.
	Max int `json:"max,omitempty"`

	// AutoscalerEnabled is true when an autoscale: block is declared.
	AutoscalerEnabled bool `json:"autoscalerEnabled"`

	// OverrideActive is true when an autoscale override is currently applied.
	OverrideActive bool `json:"overrideActive,omitempty"`

	// OverrideWorkers is the current override value when OverrideActive is true.
	OverrideWorkers int `json:"overrideWorkers,omitempty"`

	// QueueDepth is the current number of items in the workqueue.
	QueueDepth int64 `json:"queueDepth"`

	// QueueDepthConfigured is the baseline queueDepth from the CRD entry.
	QueueDepthConfigured int `json:"queueDepthConfigured"`

	// QueueDepthEffective is the current queue depth limit (baseline or override).
	QueueDepthEffective int `json:"queueDepthEffective"`

	// BusyPercent is InFlight/Effective as a percentage. Useful for UI gauges.
	BusyPercent float64 `json:"busyPercent"`

	// ResyncEffective is the current resync interval (baseline or override).
	ResyncEffective string `json:"resyncEffective"`

	// ResyncConfigured is the baseline resync from the CRD entry.
	ResyncConfigured string `json:"resyncConfigured"`
}

WorkerInfo is the serializable worker state for the CRD handler API. Keyed as "workers" in the CRD detail JSON response.

func BuildWorkerInfo added in v0.1.9

func BuildWorkerInfo(
	sem *ResizableSemaphore,
	metrics *AutoMetrics,
	configured int,
	configuredQueueDepth int,
	configuredResync string,
	maxWorkers int,
	autoscalerEnabled bool,
	autoscalerState *autoscalerStateSnapshot,
) WorkerInfo

BuildWorkerInfo constructs the WorkerInfo from the live operatorbox state. Called by the CRD handler on every /katalog/{crd} request — reads are O(1).

Jump to

Keyboard shortcuts

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