package auth import ( "context" "fmt" "log" "sync" "time" ) type Pool struct { creds []*Credential cursor int mu sync.Mutex } func NewPool(creds []*Credential) *Pool { return &Pool{creds: creds} } func (p *Pool) Pick() (*Credential, error) { p.mu.Lock() defer p.mu.Unlock() n := len(p.creds) if n == 0 { return nil, fmt.Errorf("no credentials available") } for i := 0; i < n; i++ { idx := (p.cursor + i) % n cred := p.creds[idx] if !cred.IsOnCooldown() { p.cursor = (idx + 1) % n return cred, nil } } return nil, fmt.Errorf("all %d credentials are on cooldown", n) } func (p *Pool) MarkFailure(cred *Credential, statusCode int) { switch { case statusCode == 429: cred.SetCooldown(30 * time.Second) case statusCode >= 500: cred.SetCooldown(5 * time.Second) } } func (p *Pool) MarkSuccess(cred *Credential) { cred.mu.Lock() defer cred.mu.Unlock() cred.CooldownUntil = time.Time{} } func (p *Pool) RefreshExpiring(ctx context.Context) { p.mu.Lock() creds := make([]*Credential, len(p.creds)) copy(creds, p.creds) p.mu.Unlock() threshold := time.Now().Add(5 * time.Minute) for _, cred := range creds { cred.mu.Lock() needsRefresh := cred.ExpiresAt.Before(threshold) email := cred.Email cred.mu.Unlock() if needsRefresh { log.Printf("refreshing token for %s (expires %s)", email, cred.ExpiresAt.Format(time.RFC3339)) if err := RefreshToken(ctx, cred); err != nil { log.Printf("failed to refresh token for %s: %v", email, err) } else { log.Printf("refreshed token for %s, new expiry %s", email, cred.ExpiresAt.Format(time.RFC3339)) } } } }