// 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 } }