package auth import ( "context" "fmt" "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.ClearCooldown() } func (p *Pool) RefreshExpiring(ctx context.Context) { refreshExpiring(p) } func (p *Pool) RefreshAll(ctx context.Context) []map[string]string { p.mu.Lock() creds := make([]*Credential, len(p.creds)) copy(creds, p.creds) p.mu.Unlock() var results []map[string]string for _, cred := range creds { cred.mu.Lock() id := cred.ID email := cred.Email oldExpiry := cred.ExpiresAt hasRefresh := cred.RefreshToken != "" cred.mu.Unlock() r := map[string]string{ "id": id, "email": email, "old_expiry": oldExpiry.Format(time.RFC3339), } if !hasRefresh { r["status"] = "skipped" r["reason"] = "no refresh token" results = append(results, r) continue } err := RefreshToken(ctx, cred) if err != nil { r["status"] = "error" r["error"] = err.Error() } else { cred.mu.Lock() r["status"] = "ok" r["new_expiry"] = cred.ExpiresAt.Format(time.RFC3339) r["new_token_prefix"] = cred.AccessToken[:20] + "..." cred.mu.Unlock() } results = append(results, r) } return results }