proxy-pool/internal/controller/pool/reconciler.go
2026-07-30 14:15:48 +08:00

111 lines
3.0 KiB
Go

package pool
import (
"errors"
"math"
"sync"
"time"
"proxy-pool/internal/domain/upstream"
)
var ErrInvalidReconcilePolicy = errors.New("invalid pool reconcile policy")
type ReconcilePolicy struct {
MinimumAvailableSlots int64
TargetAvailableSlots int64
ExpectedPerFetch int
ExpectedSlotsPerFetch int64
SafetyMargin time.Duration
}
type FetchNotifier interface {
Notify()
}
type ReconcileDecision struct {
AvailableSlots int64
PendingExpected int
FetchedTotal int64
FetchAllowance int
EffectiveSlots int64
Triggered bool
}
type Reconciler struct {
policy ReconcilePolicy
budget *FetchBudget
notifier FetchNotifier
mu sync.Mutex
refilling bool
slotsPerProxy int64
}
func NewReconciler(policy ReconcilePolicy, budget *FetchBudget, notifier FetchNotifier) (*Reconciler, error) {
if policy.MinimumAvailableSlots <= 0 || policy.TargetAvailableSlots <= policy.MinimumAvailableSlots ||
policy.ExpectedPerFetch <= 0 || policy.ExpectedSlotsPerFetch <= 0 ||
policy.SafetyMargin < 0 || budget == nil || notifier == nil {
return nil, ErrInvalidReconcilePolicy
}
if budget.expected != policy.ExpectedPerFetch {
return nil, ErrInvalidReconcilePolicy
}
if policy.ExpectedSlotsPerFetch%int64(policy.ExpectedPerFetch) != 0 {
return nil, ErrInvalidReconcilePolicy
}
return &Reconciler{
policy: policy, budget: budget, notifier: notifier,
slotsPerProxy: policy.ExpectedSlotsPerFetch / int64(policy.ExpectedPerFetch),
}, 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()
availableSlots := inventory.AvailableSlots(now, r.policy.SafetyMargin)
pendingSlots := saturatingMultiply(int64(usage.PendingExpected), r.slotsPerProxy)
decision := ReconcileDecision{
AvailableSlots: availableSlots,
PendingExpected: usage.PendingExpected,
FetchedTotal: usage.FetchedTotal,
FetchAllowance: r.budget.FetchAllowance(),
EffectiveSlots: saturatingAdd(availableSlots, pendingSlots),
}
r.mu.Lock()
if r.refilling {
if usage.PendingExpected == 0 && decision.AvailableSlots >= r.policy.TargetAvailableSlots {
r.refilling = false
}
} else if decision.AvailableSlots < r.policy.MinimumAvailableSlots {
r.refilling = true
}
trigger := r.refilling &&
decision.EffectiveSlots < r.policy.TargetAvailableSlots &&
decision.FetchAllowance >= r.policy.ExpectedPerFetch
r.mu.Unlock()
if !trigger {
return decision
}
r.notifier.Notify()
decision.Triggered = true
return decision
}
func saturatingMultiply(left, right int64) int64 {
if left <= 0 || right <= 0 {
return 0
}
if left > math.MaxInt64/right {
return math.MaxInt64
}
return left * right
}
func saturatingAdd(left, right int64) int64 {
if left >= math.MaxInt64-right {
return math.MaxInt64
}
return left + right
}