125 lines
3.5 KiB
Go
125 lines
3.5 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 {
|
|
return r.reconcileSnapshot(
|
|
InventorySnapshot{AvailableSlots: inventory.AvailableSlots(now, r.policy.SafetyMargin)},
|
|
false,
|
|
)
|
|
}
|
|
|
|
func (r *Reconciler) ReconcileSnapshot(inventory InventorySnapshot) ReconcileDecision {
|
|
return r.reconcileSnapshot(inventory, true)
|
|
}
|
|
|
|
func (r *Reconciler) reconcileSnapshot(inventory InventorySnapshot, synchronizeManaged bool) ReconcileDecision {
|
|
usage := r.budget.Snapshot()
|
|
if synchronizeManaged && inventory.Managed >= 0 && usage.PendingExpected == 0 {
|
|
_ = r.budget.SynchronizeManaged(inventory.Managed)
|
|
usage = r.budget.Snapshot()
|
|
}
|
|
pendingSlots := saturatingMultiply(int64(usage.PendingExpected), r.slotsPerProxy)
|
|
decision := ReconcileDecision{
|
|
AvailableSlots: inventory.AvailableSlots,
|
|
PendingExpected: usage.PendingExpected,
|
|
FetchedTotal: usage.FetchedTotal,
|
|
FetchAllowance: r.budget.FetchAllowance(),
|
|
EffectiveSlots: saturatingAdd(inventory.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
|
|
}
|