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) }