Files
2026-09-01 18:25:25 +01:00

50 lines
1.6 KiB
Go

package allocator
import (
"fmt"
"sync"
"time"
)
// ErrAllocationBudgetExceeded is deliberately generic: callers should not
// learn quota internals, and the allocator can safely retry the leased match
// after the current window expires.
var ErrAllocationBudgetExceeded = fmt.Errorf("allocation budget exceeded")
// FixedWindowBudget is a process-local denial-of-wallet guard. It limits the
// number of provider allocation attempts per region in a time window. The
// production deployment must use the same policy behind a shared durable
// counter for a global quota; this type prevents one allocator replica from
// spending without bound and is useful in tests and single-replica setups.
type FixedWindowBudget struct {
mu sync.Mutex
limit int
window time.Duration
windowStart time.Time
counts map[string]int
}
func NewFixedWindowBudget(limit int, window time.Duration) (*FixedWindowBudget, error) {
if limit < 1 || window <= 0 {
return nil, fmt.Errorf("invalid allocation budget")
}
return &FixedWindowBudget{limit: limit, window: window, counts: make(map[string]int)}, nil
}
func (b *FixedWindowBudget) Allow(region string, now time.Time) error {
if b == nil || (region != "EU" && region != "NA") || now.IsZero() {
return fmt.Errorf("invalid allocation budget request")
}
b.mu.Lock()
defer b.mu.Unlock()
if b.windowStart.IsZero() || !now.Before(b.windowStart.Add(b.window)) {
b.windowStart = now
b.counts = make(map[string]int)
}
if b.counts[region] >= b.limit {
return ErrAllocationBudgetExceeded
}
b.counts[region]++
return nil
}