persona-community-1/pkg/routing/cooldown.go
jordan 4004f88f4a
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 10:20:59 +00:00

151 lines
5.0 KiB
Go

package routing
import "time"
// Default cooldown periods.
const (
// DefaultCooldownPeriod is the cooldown for rate limits and quota errors.
// 1 hour gives provider APIs time to reset their quotas.
DefaultCooldownPeriod = 1 * time.Hour
// TransientCooldownPeriod is the cooldown for transient server errors (5xx).
// These errors typically resolve quickly, so we use a shorter cooldown to
// trigger fallback but allow quick recovery.
TransientCooldownPeriod = 30 * time.Second
)
// CooldownTracker defines the interface for tracking provider cooldowns.
// CircuitBreaker (in-memory) implements this interface. Additional implementations
// (e.g., file-based persistence) can be added via CombinedCooldown.
//
// IMPORTANT: All cooldown tracking in the codebase MUST use this interface.
// Do NOT implement custom cooldown tracking elsewhere.
type CooldownTracker interface {
// IsAvailable returns true if the provider is not in cooldown.
IsAvailable(providerName string) bool
// CooldownRemaining returns time until cooldown expires (0 if available).
CooldownRemaining(providerName string) time.Duration
// RecordFailure records a failure and potentially enters cooldown.
// Returns true if the provider entered cooldown.
// Note: Exempt providers (ExemptProviders) never enter cooldown.
RecordFailure(providerName string, err error) bool
// Reset removes a provider from cooldown immediately.
Reset(providerName string)
// ResetAll clears all cooldowns.
ResetAll()
}
// CombinedCooldown wraps multiple CooldownTrackers, checking all of them.
// A provider is only available if ALL trackers report it as available.
//
// Typical usage: combine multiple CooldownTracker implementations
// (e.g., in-memory CircuitBreaker with custom persistence) for layered tracking.
type CombinedCooldown struct {
trackers []CooldownTracker
}
// NewCombinedCooldown creates a tracker that combines multiple sources.
// Pass nil trackers to skip them (they will be filtered out).
func NewCombinedCooldown(trackers ...CooldownTracker) *CombinedCooldown {
// Filter out nil trackers
nonNil := make([]CooldownTracker, 0, len(trackers))
for _, t := range trackers {
if t != nil {
nonNil = append(nonNil, t)
}
}
return &CombinedCooldown{trackers: nonNil}
}
// IsAvailable returns true only if ALL trackers report the provider as available.
func (c *CombinedCooldown) IsAvailable(providerName string) bool {
for _, t := range c.trackers {
if !t.IsAvailable(providerName) {
return false
}
}
return true
}
// CooldownRemaining returns the maximum remaining cooldown from all trackers.
func (c *CombinedCooldown) CooldownRemaining(providerName string) time.Duration {
var max time.Duration
for _, t := range c.trackers {
if remaining := t.CooldownRemaining(providerName); remaining > max {
max = remaining
}
}
return max
}
// RecordFailure records the failure to all trackers.
// Returns true if any tracker opened a cooldown.
func (c *CombinedCooldown) RecordFailure(providerName string, err error) bool {
var anyOpened bool
for _, t := range c.trackers {
if t.RecordFailure(providerName, err) {
anyOpened = true
}
}
return anyOpened
}
// Reset removes the provider from cooldown in all trackers.
func (c *CombinedCooldown) Reset(providerName string) {
for _, t := range c.trackers {
t.Reset(providerName)
}
}
// ResetAll clears all cooldowns in all trackers.
func (c *CombinedCooldown) ResetAll() {
for _, t := range c.trackers {
t.ResetAll()
}
}
// Compile-time interface check
var _ CooldownTracker = (*CombinedCooldown)(nil)
// CooldownConfig holds the configuration for building a CooldownTracker.
// This struct is used by mediagen.Manager and textgen.Manager to configure
// their cooldown behavior.
//
// Usage: Provide a CircuitBreaker for in-memory tracking, or leave nil
// to auto-create one with the specified CooldownPeriod.
type CooldownConfig struct {
// CircuitBreaker provides in-memory cooldown tracking.
// Good for long-running services where state is maintained in memory.
// If nil, a default CircuitBreaker is created with CooldownPeriod.
CircuitBreaker *CircuitBreaker
// CooldownPeriod is the default cooldown duration for rate-limited providers.
// Only used when creating a default CircuitBreaker (when CircuitBreaker is nil).
// Defaults to DefaultCooldownPeriod (1 hour) if zero.
CooldownPeriod time.Duration
}
// BuildCooldownTracker creates a CooldownTracker from the configuration.
// This is the standard way to construct cooldown trackers in the codebase.
//
// Logic:
// 1. If CircuitBreaker is provided, returns it directly
// 2. If CircuitBreaker is nil, creates a default one with the specified
// cooldown period (or DefaultCooldownPeriod if zero)
func BuildCooldownTracker(config CooldownConfig) CooldownTracker {
if config.CircuitBreaker != nil {
return config.CircuitBreaker
}
// Create default circuit breaker
cooldown := config.CooldownPeriod
if cooldown == 0 {
cooldown = DefaultCooldownPeriod
}
return NewCircuitBreaker(cooldown)
}