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 }