// Package keymanager provides high-performance API key management package keymanager import ( "bufio" "os" "regexp" "runtime" "strings" "sync" "sync/atomic" "time" "gpt-load/internal/errors" "gpt-load/internal/types" "github.com/sirupsen/logrus" ) // Manager implements the KeyManager interface type Manager struct { keysFilePath string keys []string keyPreviews []string currentIndex int64 blacklistedKeys sync.Map successCount int64 failureCount int64 keyFailureCounts sync.Map config types.KeysConfig // Performance optimization: pre-compiled regex patterns permanentErrorPatterns []*regexp.Regexp // Memory management cleanupTicker *time.Ticker stopCleanup chan bool // Read-write lock to protect key list keysMutex sync.RWMutex } // NewManager creates a new key manager func NewManager(config types.KeysConfig) (types.KeyManager, error) { if config.FilePath == "" { return nil, errors.NewAppError(errors.ErrKeyFileNotFound, "Keys file path is required") } km := &Manager{ keysFilePath: config.FilePath, currentIndex: int64(config.StartIndex), stopCleanup: make(chan bool), config: config, // Pre-compile regex patterns permanentErrorPatterns: []*regexp.Regexp{ regexp.MustCompile(`(?i)invalid api key`), regexp.MustCompile(`(?i)incorrect api key`), regexp.MustCompile(`(?i)api key not found`), regexp.MustCompile(`(?i)unauthorized`), regexp.MustCompile(`(?i)account deactivated`), regexp.MustCompile(`(?i)billing`), }, } // Start memory cleanup km.setupMemoryCleanup() // Load keys if err := km.LoadKeys(); err != nil { return nil, err } return km, nil } // LoadKeys loads API keys from file func (km *Manager) LoadKeys() error { file, err := os.Open(km.keysFilePath) if err != nil { return errors.NewAppErrorWithCause(errors.ErrKeyFileNotFound, "Failed to open keys file", err) } defer file.Close() var keys []string var keyPreviews []string scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line != "" && !strings.HasPrefix(line, "#") { keys = append(keys, line) // Create preview (first 8 chars + "..." + last 4 chars) if len(line) > 12 { preview := line[:8] + "..." + line[len(line)-4:] keyPreviews = append(keyPreviews, preview) } else { keyPreviews = append(keyPreviews, line) } } } if err := scanner.Err(); err != nil { return errors.NewAppErrorWithCause(errors.ErrKeyFileInvalid, "Failed to read keys file", err) } if len(keys) == 0 { return errors.NewAppError(errors.ErrNoKeysAvailable, "No valid API keys found in file") } km.keysMutex.Lock() km.keys = keys km.keyPreviews = keyPreviews km.keysMutex.Unlock() logrus.Infof("Successfully loaded %d API keys", len(keys)) return nil } // GetNextKey gets the next available key (high-performance version) func (km *Manager) GetNextKey() (*types.KeyInfo, error) { km.keysMutex.RLock() keysLen := len(km.keys) if keysLen == 0 { km.keysMutex.RUnlock() return nil, errors.ErrNoAPIKeysAvailable } // Fast path: directly get next key, avoid blacklist check overhead currentIdx := atomic.AddInt64(&km.currentIndex, 1) - 1 keyIndex := int(currentIdx) % keysLen selectedKey := km.keys[keyIndex] keyPreview := km.keyPreviews[keyIndex] km.keysMutex.RUnlock() // Check if blacklisted if _, blacklisted := km.blacklistedKeys.Load(selectedKey); !blacklisted { return &types.KeyInfo{ Key: selectedKey, Index: keyIndex, Preview: keyPreview, }, nil } // Slow path: find next available key return km.findNextAvailableKey(keyIndex, keysLen) } // findNextAvailableKey finds the next available non-blacklisted key func (km *Manager) findNextAvailableKey(startIndex, keysLen int) (*types.KeyInfo, error) { km.keysMutex.RLock() defer km.keysMutex.RUnlock() blacklistedCount := 0 for i := 0; i < keysLen; i++ { keyIndex := (startIndex + i) % keysLen selectedKey := km.keys[keyIndex] if _, blacklisted := km.blacklistedKeys.Load(selectedKey); !blacklisted { return &types.KeyInfo{ Key: selectedKey, Index: keyIndex, Preview: km.keyPreviews[keyIndex], }, nil } blacklistedCount++ } if blacklistedCount >= keysLen { logrus.Warn("All keys are blacklisted, resetting blacklist") km.blacklistedKeys = sync.Map{} km.keyFailureCounts = sync.Map{} // Return first key after reset firstKey := km.keys[0] firstPreview := km.keyPreviews[0] return &types.KeyInfo{ Key: firstKey, Index: 0, Preview: firstPreview, }, nil } return nil, errors.ErrAllAPIKeysBlacklisted } // RecordSuccess records successful key usage func (km *Manager) RecordSuccess(key string) { atomic.AddInt64(&km.successCount, 1) // Reset failure count for this key on success km.keyFailureCounts.Delete(key) } // RecordFailure records key failure and potentially blacklists it func (km *Manager) RecordFailure(key string, err error) { atomic.AddInt64(&km.failureCount, 1) // Check if this is a permanent error if km.isPermanentError(err) { km.blacklistedKeys.Store(key, time.Now()) logrus.Debugf("Key blacklisted due to permanent error: %v", err) return } // Increment failure count failCount, _ := km.keyFailureCounts.LoadOrStore(key, new(int64)) if counter, ok := failCount.(*int64); ok { newFailCount := atomic.AddInt64(counter, 1) // Blacklist if threshold exceeded if int(newFailCount) >= km.config.BlacklistThreshold { km.blacklistedKeys.Store(key, time.Now()) logrus.Debugf("Key blacklisted after %d failures", newFailCount) } } } // isPermanentError checks if an error is permanent func (km *Manager) isPermanentError(err error) bool { if err == nil { return false } errorStr := strings.ToLower(err.Error()) for _, pattern := range km.permanentErrorPatterns { if pattern.MatchString(errorStr) { return true } } return false } // GetStats returns current statistics func (km *Manager) GetStats() types.Stats { km.keysMutex.RLock() totalKeys := len(km.keys) km.keysMutex.RUnlock() blacklistedCount := 0 km.blacklistedKeys.Range(func(key, value any) bool { blacklistedCount++ return true }) var m runtime.MemStats runtime.ReadMemStats(&m) return types.Stats{ CurrentIndex: atomic.LoadInt64(&km.currentIndex), TotalKeys: totalKeys, HealthyKeys: totalKeys - blacklistedCount, BlacklistedKeys: blacklistedCount, SuccessCount: atomic.LoadInt64(&km.successCount), FailureCount: atomic.LoadInt64(&km.failureCount), MemoryUsage: types.MemoryUsage{ Alloc: m.Alloc, TotalAlloc: m.TotalAlloc, Sys: m.Sys, NumGC: m.NumGC, LastGCTime: time.Unix(0, int64(m.LastGC)).Format("2006-01-02 15:04:05"), NextGCTarget: m.NextGC, }, } } // ResetBlacklist resets the blacklist func (km *Manager) ResetBlacklist() { km.blacklistedKeys = sync.Map{} km.keyFailureCounts = sync.Map{} logrus.Info("Blacklist reset successfully") } // GetBlacklist returns current blacklisted keys func (km *Manager) GetBlacklist() []types.BlacklistEntry { var blacklist []types.BlacklistEntry km.blacklistedKeys.Range(func(key, value any) bool { keyStr := key.(string) blacklistTime := value.(time.Time) // Create preview preview := keyStr if len(keyStr) > 12 { preview = keyStr[:8] + "..." + keyStr[len(keyStr)-4:] } // Get failure count failCount := 0 if count, exists := km.keyFailureCounts.Load(keyStr); exists { failCount = int(atomic.LoadInt64(count.(*int64))) } blacklist = append(blacklist, types.BlacklistEntry{ Key: keyStr, Preview: preview, Reason: "Exceeded failure threshold", BlacklistAt: blacklistTime, FailCount: failCount, }) return true }) return blacklist } // setupMemoryCleanup sets up periodic memory cleanup func (km *Manager) setupMemoryCleanup() { // Reduce GC frequency to every 15 minutes to avoid performance impact km.cleanupTicker = time.NewTicker(15 * time.Minute) go func() { for { select { case <-km.cleanupTicker.C: // Only trigger GC if memory usage is high var m runtime.MemStats runtime.ReadMemStats(&m) // Trigger GC only if allocated memory is above 100MB if m.Alloc > 100*1024*1024 { runtime.GC() logrus.Debugf("Manual GC triggered, memory usage: %d MB", m.Alloc/1024/1024) } case <-km.stopCleanup: return } } }() } // Close closes the key manager and cleans up resources func (km *Manager) Close() { if km.cleanupTicker != nil { km.cleanupTicker.Stop() } close(km.stopCleanup) }