104 lines
3.2 KiB
Go
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
|
|
}
|
|
}
|