151 lines
5.0 KiB
Go
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)
|
|
}
|