61 lines
1.4 KiB
Go
61 lines
1.4 KiB
Go
package processing
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"sync"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/google/uuid"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// StartLimiter lightly rate-limits processing job starts per company.
|
|||
|
|
// In-process only — not shared across API replicas (effective RPM ≈ N × replicas).
|
|||
|
|
// RATE_LIMIT_REPLICAS does not divide this limiter; multi-replica hard caps need edge/WAF.
|
|||
|
|
// Counts StartJob/RetryJob API calls once each — not per auto-split sibling job,
|
|||
|
|
// and not per product in a bulk StartJob payload (capped by MaxStartProducts).
|
|||
|
|
type StartLimiter struct {
|
|||
|
|
mu sync.Mutex
|
|||
|
|
window time.Duration
|
|||
|
|
max int
|
|||
|
|
events map[uuid.UUID][]time.Time
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewStartLimiter allows maxStarts per window (e.g. 20/min).
|
|||
|
|
func NewStartLimiter(maxStarts int, window time.Duration) *StartLimiter {
|
|||
|
|
if maxStarts <= 0 {
|
|||
|
|
maxStarts = 20
|
|||
|
|
}
|
|||
|
|
if window <= 0 {
|
|||
|
|
window = time.Minute
|
|||
|
|
}
|
|||
|
|
return &StartLimiter{
|
|||
|
|
window: window,
|
|||
|
|
max: maxStarts,
|
|||
|
|
events: make(map[uuid.UUID][]time.Time),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Allow reports whether a new job start is permitted for companyID.
|
|||
|
|
func (l *StartLimiter) Allow(companyID uuid.UUID) bool {
|
|||
|
|
if l == nil {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
now := time.Now()
|
|||
|
|
l.mu.Lock()
|
|||
|
|
defer l.mu.Unlock()
|
|||
|
|
cut := now.Add(-l.window)
|
|||
|
|
ev := l.events[companyID]
|
|||
|
|
kept := ev[:0]
|
|||
|
|
for _, t := range ev {
|
|||
|
|
if t.After(cut) {
|
|||
|
|
kept = append(kept, t)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if len(kept) >= l.max {
|
|||
|
|
l.events[companyID] = kept
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
kept = append(kept, now)
|
|||
|
|
l.events[companyID] = kept
|
|||
|
|
return true
|
|||
|
|
}
|