package pool import ( "errors" "time" "github.com/proxy-pool/proxy-pool/internal/domain/upstream" ) var ErrInvalidReconcilePolicy = errors.New("invalid pool reconcile policy") type ReconcilePolicy struct { MinimumAvailableSlots int64 ExpectedPerFetch int SafetyMargin time.Duration } type FetchNotifier interface { Notify() } type ReconcileDecision struct { AvailableSlots int64 PendingExpected int FetchedTotal int64 FetchAllowance int Triggered bool } type Reconciler struct { policy ReconcilePolicy budget *FetchBudget notifier FetchNotifier } func NewReconciler(policy ReconcilePolicy, budget *FetchBudget, notifier FetchNotifier) (*Reconciler, error) { if policy.MinimumAvailableSlots <= 0 || policy.ExpectedPerFetch <= 0 || policy.SafetyMargin < 0 || budget == nil || notifier == nil { return nil, ErrInvalidReconcilePolicy } if budget.expected != policy.ExpectedPerFetch { return nil, ErrInvalidReconcilePolicy } return &Reconciler{policy: policy, budget: budget, notifier: notifier}, nil } // Reconcile centralizes the cold-path decision. The notifier may coalesce many // calls; Provider Reconciler atomically reserves the budget before doing I/O. func (r *Reconciler) Reconcile(now time.Time, inventory upstream.Inventory) ReconcileDecision { usage := r.budget.Snapshot() decision := ReconcileDecision{ AvailableSlots: inventory.AvailableSlots(now, r.policy.SafetyMargin), PendingExpected: usage.PendingExpected, FetchedTotal: usage.FetchedTotal, FetchAllowance: r.budget.FetchAllowance(), } if decision.AvailableSlots >= r.policy.MinimumAvailableSlots || decision.FetchAllowance < r.policy.ExpectedPerFetch { return decision } r.notifier.Notify() decision.Triggered = true return decision }