proxy-pool/internal/controller/health/reducer.go
youfak bea790f1d4
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: add controller health reducer
2026-07-31 18:13:47 +08:00

104 lines
3.2 KiB
Go

// Package health owns Controller-side reduction of Checker facts. It never
// probes, schedules, or persists raw observations.
package health
import (
"context"
"errors"
"reflect"
"proxy-pool/internal/domain/activitypool"
healthDomain "proxy-pool/internal/domain/health"
)
var (
ErrInvalidReducer = errors.New("invalid health reducer")
ErrInvalidThreshold = errors.New("invalid health failure threshold")
)
// FailureThresholdResolver selects the configured consecutive-failure limit
// for one normalized fact. It keeps policy lookup outside the Checker and
// lets a Controller use different limits for different upstreams.
type FailureThresholdResolver func(context.Context, healthDomain.Observation) (int, error)
// Result contains only the reduced state written by the corresponding store.
// Exactly one of Global or Target is non-nil for an accepted observation.
type Result struct {
Global *activitypool.Entry
Target *healthDomain.TargetState
}
// Reducer is the Controller's narrow authority boundary for checker facts.
// The stores commit their own atomic idempotency and ordering semantics.
type Reducer struct {
global activitypool.GlobalHealthStore
target activitypool.TargetHealthStore
threshold FailureThresholdResolver
}
func NewReducer(
global activitypool.GlobalHealthStore,
target activitypool.TargetHealthStore,
threshold FailureThresholdResolver,
) (*Reducer, error) {
if nilInterface(global) || nilInterface(target) || threshold == nil {
return nil, ErrInvalidReducer
}
return &Reducer{global: global, target: target, threshold: threshold}, nil
}
func nilInterface(value any) bool {
if value == nil {
return true
}
reflected := reflect.ValueOf(value)
switch reflected.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return reflected.IsNil()
default:
return false
}
}
// Apply normalizes one immutable Checker fact, resolves its configured policy,
// and commits it through exactly one atomic activity-pool store method.
func (r *Reducer) Apply(ctx context.Context, observation healthDomain.Observation) (Result, error) {
if ctx == nil || r == nil || r.global == nil || r.target == nil || r.threshold == nil {
return Result{}, ErrInvalidReducer
}
if err := ctx.Err(); err != nil {
return Result{}, err
}
normalized, err := healthDomain.NormalizeObservation(observation)
if err != nil {
return Result{}, err
}
maxConsecutiveFailures, err := r.threshold(ctx, normalized)
if err != nil {
return Result{}, err
}
if maxConsecutiveFailures <= 0 {
return Result{}, ErrInvalidThreshold
}
switch normalized.Level {
case healthDomain.LevelBasic, healthDomain.LevelEgress:
entry, err := r.global.ApplyGlobalObservation(ctx, activitypool.GlobalHealthCommand{
Observation: normalized, MaxConsecutiveFailures: maxConsecutiveFailures,
})
if err != nil {
return Result{}, err
}
return Result{Global: &entry}, nil
case healthDomain.LevelTarget:
state, err := r.target.ApplyTargetObservation(ctx, activitypool.TargetHealthCommand{
Observation: normalized, MaxConsecutiveFailures: maxConsecutiveFailures,
})
if err != nil {
return Result{}, err
}
return Result{Target: &state}, nil
default:
return Result{}, healthDomain.ErrInvalidObservation
}
}