Documentation
¶
Overview ¶
Package adaptive implements a dual-engine controller that dynamically switches between io_uring and epoll based on runtime telemetry.
The adaptive Engine starts both sub-engines on the same port (via SO_REUSEPORT) and periodically evaluates their performance scores. If the standby engine's historical score exceeds the active engine's score by more than a threshold, the controller triggers a switch. Oscillation detection locks switching for five minutes after three rapid switches.
Users select the adaptive engine via celeris.Config{Engine: celeris.Adaptive}. It is the default engine on Linux.
Index ¶
- type Engine
- func (e *Engine) ActiveEngine() engine.Engine
- func (e *Engine) Addr() net.Addr
- func (e *Engine) DriverFDCount() int
- func (e *Engine) ForceSwitch()
- func (e *Engine) FreezeSwitching()
- func (e *Engine) Listen(ctx context.Context) error
- func (e *Engine) Metrics() engine.EngineMetrics
- func (e *Engine) NumWorkers() int
- func (e *Engine) SetFreezeCooldown(d time.Duration)
- func (e *Engine) Shutdown(ctx context.Context) error
- func (e *Engine) SwitchRejectedCount() uint64
- func (e *Engine) Type() engine.EngineType
- func (e *Engine) UnfreezeSwitching()
- func (e *Engine) WorkerLoop(n int) engine.WorkerLoop
- type ScoreWeights
- type TelemetrySampler
- type TelemetrySnapshot
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is an adaptive meta-engine that switches between io_uring and epoll.
func New ¶
New creates a new adaptive engine with epoll as primary and io_uring as secondary. Epoll starts first because it has lower H2 latency on current kernels (single-pass read→process→write vs io_uring's two-iteration CQE model). The controller may switch to io_uring if telemetry indicates it would perform better for the workload. Both sub-engines get the full resource config. This is safe because standby workers are fully suspended (zero CPU, zero connections, listen sockets closed).
cpuMon is an engine.CPUMonitor (the public interface); when non-nil it supplies the live sampler with CPU utilization data so the io_uring bias can fire in the empirical sweet spot. External callers can pass their own implementation or the built-in /proc/stat monitor. Pass nil for tests or when CPU monitoring is not available; the sampler degrades gracefully with CPUUtilization=0 in the snapshot.
Example ¶
ExampleNew shows that adaptive.New accepts any engine.CPUMonitor, including one defined entirely outside the celeris module. No Output directive: this example documents the public signature and is compiled, not executed (it would need a live io_uring/epoll-capable kernel to run).
//go:build linux
package main
import (
"context"
"time"
"github.com/goceleris/celeris/adaptive"
"github.com/goceleris/celeris/engine"
"github.com/goceleris/celeris/protocol/h2/stream"
"github.com/goceleris/celeris/resource"
)
// fakeCPUMonitor is an external implementation of the PUBLIC engine.CPUMonitor
// interface — proof that callers outside the module can supply their own
// monitor without depending on the internal cpumon package.
type fakeCPUMonitor struct{ util float64 }
func (f fakeCPUMonitor) Sample() (engine.CPUSample, error) {
return engine.CPUSample{Utilization: f.util, Timestamp: time.Now()}, nil
}
// ExampleNew shows that adaptive.New accepts any engine.CPUMonitor, including
// one defined entirely outside the celeris module. No Output directive: this
// example documents the public signature and is compiled, not executed (it
// would need a live io_uring/epoll-capable kernel to run).
func main() {
var mon engine.CPUMonitor = fakeCPUMonitor{util: 0.9}
cfg := resource.Config{Addr: ":0", Protocol: engine.HTTP1}
handler := stream.HandlerFunc(func(_ context.Context, _ *stream.Stream) error { return nil })
eng, err := adaptive.New(cfg, handler, mon)
if err != nil {
return
}
_ = eng
}
Output:
func (*Engine) ActiveEngine ¶
ActiveEngine returns the currently active engine.
func (*Engine) DriverFDCount ¶ added in v1.4.0
DriverFDCount reports the number of driver FDs currently registered on either sub-engine. Exposed for tests and observability.
func (*Engine) ForceSwitch ¶ added in v0.3.5
func (e *Engine) ForceSwitch()
ForceSwitch triggers an immediate engine switch (for testing).
func (*Engine) FreezeSwitching ¶
func (e *Engine) FreezeSwitching()
FreezeSwitching prevents the controller from switching engines.
FreezeSwitching is reference-counted: every call must be matched by a corresponding UnfreezeSwitching. The engine remains frozen until every external freeze has been released AND every driver-registered FD has been unregistered. This makes it safe for benchmarks and drivers to hold independent freezes without clobbering each other.
func (*Engine) Metrics ¶
func (e *Engine) Metrics() engine.EngineMetrics
Metrics aggregates metrics from both sub-engines.
func (*Engine) NumWorkers ¶ added in v1.4.0
NumWorkers delegates to the currently active sub-engine's EventLoopProvider. Returns 0 if the active engine does not implement EventLoopProvider.
func (*Engine) SetFreezeCooldown ¶ added in v1.1.0
SetFreezeCooldown sets the duration to suppress further switches after a switch. Zero disables the cooldown (default). This prevents oscillation under unstable load.
func (*Engine) Shutdown ¶
Shutdown gracefully shuts down both sub-engines.
It first cancels and JOINS the goroutines started by Listen (the evaluation loop and worker scaler), so that no controller tick can still be sampling telemetry — including the CPU monitor the server closes immediately after Shutdown returns — by the time this function completes. Only then are the sub-engines shut down. This is purely a join/sequencing concern; it does not touch the ACTIVE→DRAINING→SUSPENDED worker lifecycle.
func (*Engine) SwitchRejectedCount ¶ added in v1.4.0
SwitchRejectedCount reports how many engine-switch attempts were blocked by outstanding driver FDs since the engine started. Monotonic; useful for tests asserting that a switch actually happened (or did not).
func (*Engine) UnfreezeSwitching ¶
func (e *Engine) UnfreezeSwitching()
UnfreezeSwitching releases one external freeze. The engine only becomes thawed when the external freeze count and the driver-FD count both reach zero. Calling UnfreezeSwitching more times than FreezeSwitching is a no-op (and does NOT unfreeze the engine if drivers still hold FDs).
func (*Engine) WorkerLoop ¶ added in v1.4.0
func (e *Engine) WorkerLoop(n int) engine.WorkerLoop
WorkerLoop returns a driver-safe wrapper around the currently active sub-engine's worker N. The wrapper:
- Re-fetches the active sub-engine on every RegisterConn, so the FD lands on whichever sub-engine is active at that instant — this matters only during the tiny window between one register/unregister cycle and the next (the refcount pins frozen while any FD lives).
- Pins the specific inner engine.WorkerLoop each FD was registered on so Write/Unregister always go to the right sub-engine.
- Increments the engine's driver-FD refcount on RegisterConn and decrements on UnregisterConn, which keeps [Engine.performSwitch] from swapping sub-engines while any FD is live.
Callers never need to call Engine.FreezeSwitching manually.
type ScoreWeights ¶
type ScoreWeights struct {
// Throughput is the weight applied to requests-per-second in the score.
Throughput float64
// ErrorRate is the penalty weight applied to the error fraction.
ErrorRate float64
}
ScoreWeights defines the weighting for each telemetry signal in score computation. Higher throughput weight favors faster engines; higher error weight penalizes unreliable ones.
func DefaultWeights ¶
func DefaultWeights() ScoreWeights
DefaultWeights returns the default score weights.
type TelemetrySampler ¶
type TelemetrySampler interface {
Sample(e engine.Engine) TelemetrySnapshot
}
TelemetrySampler produces telemetry snapshots from an engine.
type TelemetrySnapshot ¶
type TelemetrySnapshot struct {
// Timestamp is when this snapshot was taken.
Timestamp time.Time
// ThroughputRPS is the recent requests-per-second rate.
ThroughputRPS float64
// ErrorRate is the fraction of requests that resulted in errors (0.0-1.0).
ErrorRate float64
// ActiveConnections is the current number of open connections.
ActiveConnections int64
// CPUUtilization is the estimated CPU usage fraction (0.0-1.0). Read from
// the live sampler's CPUMonitor; zero when no monitor is wired.
CPUUtilization float64
}
TelemetrySnapshot captures a point-in-time view of engine performance, used by the controller to decide whether to switch engines.