265 lines
6.7 KiB
Go
265 lines
6.7 KiB
Go
package balancer
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/chenxin/hunjing/mcp/web-search/internal/config"
|
|
"github.com/chenxin/hunjing/mcp/web-search/internal/models"
|
|
)
|
|
|
|
// KeySearcher 按密钥执行搜索的接口。
|
|
// 与 SearchProvider 解耦,方便测试时注入 mock。
|
|
type KeySearcher interface {
|
|
Search(ctx context.Context, apiKey string, req *models.SearchRequest) (*models.SearchResult, error)
|
|
HealthCheck(ctx context.Context) error
|
|
Name() models.ProviderKind
|
|
}
|
|
|
|
// LoadBalancer 负载均衡器(goroutine-safe)。
|
|
type LoadBalancer struct {
|
|
pools map[models.ProviderKind]*KeyPool
|
|
searchers map[models.ProviderKind]KeySearcher
|
|
disabled map[models.ProviderKind]bool // 运行时动态禁用
|
|
retryCount int
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewLoadBalancer(cfg *config.BalancerConfig) *LoadBalancer {
|
|
return &LoadBalancer{
|
|
pools: make(map[models.ProviderKind]*KeyPool),
|
|
searchers: make(map[models.ProviderKind]KeySearcher),
|
|
disabled: make(map[models.ProviderKind]bool),
|
|
retryCount: cfg.RetryCount,
|
|
}
|
|
}
|
|
|
|
// AddProvider 注册 provider 及其密钥池。
|
|
func (lb *LoadBalancer) AddProvider(
|
|
provider models.ProviderKind,
|
|
keys []string,
|
|
weight int,
|
|
searcher KeySearcher,
|
|
) {
|
|
lb.mu.Lock()
|
|
defer lb.mu.Unlock()
|
|
|
|
cbThreshold := 5
|
|
cbCooldown := 30 * time.Second
|
|
lb.pools[provider] = NewKeyPool(provider, keys, weight, cbThreshold, cbCooldown)
|
|
lb.searchers[provider] = searcher
|
|
// 添加时默认启用
|
|
delete(lb.disabled, provider)
|
|
}
|
|
|
|
// RemoveProvider 移除 provider 及其密钥池。
|
|
func (lb *LoadBalancer) RemoveProvider(provider models.ProviderKind) {
|
|
lb.mu.Lock()
|
|
defer lb.mu.Unlock()
|
|
delete(lb.pools, provider)
|
|
delete(lb.searchers, provider)
|
|
delete(lb.disabled, provider)
|
|
}
|
|
|
|
// EnableProvider 启用 provider。
|
|
func (lb *LoadBalancer) EnableProvider(provider models.ProviderKind) {
|
|
lb.mu.Lock()
|
|
defer lb.mu.Unlock()
|
|
delete(lb.disabled, provider)
|
|
}
|
|
|
|
// DisableProvider 禁用 provider(搜索时跳过,不删除配置)。
|
|
func (lb *LoadBalancer) DisableProvider(provider models.ProviderKind) {
|
|
lb.mu.Lock()
|
|
defer lb.mu.Unlock()
|
|
lb.disabled[provider] = true
|
|
}
|
|
|
|
// IsProviderEnabled 检查 provider 是否启用。
|
|
func (lb *LoadBalancer) IsProviderEnabled(provider models.ProviderKind) bool {
|
|
lb.mu.RLock()
|
|
defer lb.mu.RUnlock()
|
|
return !lb.disabled[provider]
|
|
}
|
|
|
|
// SetRetryCount 更新重试次数。
|
|
func (lb *LoadBalancer) SetRetryCount(n int) {
|
|
lb.mu.Lock()
|
|
defer lb.mu.Unlock()
|
|
lb.retryCount = n
|
|
}
|
|
|
|
// HasProvider 检查 provider 是否已注册。
|
|
func (lb *LoadBalancer) HasProvider(provider models.ProviderKind) bool {
|
|
lb.mu.RLock()
|
|
defer lb.mu.RUnlock()
|
|
_, ok := lb.pools[provider]
|
|
return ok
|
|
}
|
|
|
|
// Search 执行搜索(自动选密钥、跨 provider 重试)。
|
|
func (lb *LoadBalancer) Search(ctx context.Context, req *models.SearchRequest) (*models.SearchResult, error) {
|
|
providers := lb.resolveProviders(req.Providers)
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt <= lb.getRetryCount(); attempt++ {
|
|
for _, prov := range providers {
|
|
pool, ok := lb.pools[prov]
|
|
if !ok {
|
|
continue
|
|
}
|
|
ks := pool.Select()
|
|
if ks == nil {
|
|
continue
|
|
}
|
|
searcher := lb.searchers[prov]
|
|
if searcher == nil {
|
|
continue
|
|
}
|
|
|
|
start := time.Now()
|
|
result, err := searcher.Search(ctx, ks.Key, req)
|
|
duration := time.Since(start)
|
|
|
|
if err != nil {
|
|
ks.RecordFailure()
|
|
lastErr = err
|
|
continue
|
|
}
|
|
ks.RecordSuccess(duration)
|
|
result.Provider = prov
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
if lastErr != nil {
|
|
return nil, fmt.Errorf("balancer: all providers exhausted: %w", lastErr)
|
|
}
|
|
return nil, fmt.Errorf("balancer: no available provider or key for query: %s", req.Query)
|
|
}
|
|
|
|
// SearchAll 并发搜索所有启用 provider(用于 Merge 模式)。
|
|
func (lb *LoadBalancer) SearchAll(ctx context.Context, req *models.SearchRequest) []*models.SearchResult {
|
|
providers := lb.resolveProviders(nil)
|
|
type result struct {
|
|
prov models.ProviderKind
|
|
res *models.SearchResult
|
|
err error
|
|
}
|
|
|
|
ch := make(chan result, len(providers))
|
|
for _, prov := range providers {
|
|
p := prov
|
|
go func() {
|
|
pool, ok := lb.pools[p]
|
|
if !ok {
|
|
ch <- result{prov: p, err: fmt.Errorf("no pool")}
|
|
return
|
|
}
|
|
ks := pool.Select()
|
|
if ks == nil {
|
|
ch <- result{prov: p, err: fmt.Errorf("no available key")}
|
|
return
|
|
}
|
|
searcher := lb.searchers[p]
|
|
if searcher == nil {
|
|
ch <- result{prov: p, err: fmt.Errorf("no searcher")}
|
|
return
|
|
}
|
|
start := time.Now()
|
|
res, err := searcher.Search(ctx, ks.Key, req)
|
|
duration := time.Since(start)
|
|
if err != nil {
|
|
ks.RecordFailure()
|
|
ch <- result{prov: p, err: err}
|
|
return
|
|
}
|
|
ks.RecordSuccess(duration)
|
|
res.Provider = p
|
|
ch <- result{prov: p, res: res}
|
|
}()
|
|
}
|
|
|
|
var results []*models.SearchResult
|
|
for range providers {
|
|
r := <-ch
|
|
if r.res != nil {
|
|
results = append(results, r.res)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
// ActiveKeyCount 返回每个 provider 的活跃密钥数。
|
|
// ActiveKeyCount 返回每个 provider 的活跃密钥数。
|
|
// ActiveKeyCount 返回每个 provider 的活跃密钥数。
|
|
func (lb *LoadBalancer) ActiveKeyCount() map[models.ProviderKind]int {
|
|
lb.mu.RLock()
|
|
defer lb.mu.RUnlock()
|
|
out := make(map[models.ProviderKind]int)
|
|
for p, pool := range lb.pools {
|
|
if lb.disabled[p] {
|
|
continue
|
|
}
|
|
out[p] = pool.ActiveCount()
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Providers 返回所有已注册的 provider 名称(无论启用与否)。
|
|
func (lb *LoadBalancer) Providers() []models.ProviderKind {
|
|
lb.mu.RLock()
|
|
defer lb.mu.RUnlock()
|
|
out := make([]models.ProviderKind, 0, len(lb.pools))
|
|
for p := range lb.pools {
|
|
out = append(out, p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HealthCheck 对指定 provider 执行健康检查(委托给 KeySearcher)。
|
|
func (lb *LoadBalancer) HealthCheck(ctx context.Context, provider models.ProviderKind) error {
|
|
lb.mu.RLock()
|
|
searcher, ok := lb.searchers[provider]
|
|
lb.mu.RUnlock()
|
|
if !ok {
|
|
return fmt.Errorf("healthcheck: provider not found: %s", provider)
|
|
}
|
|
return searcher.HealthCheck(ctx)
|
|
}
|
|
|
|
func (lb *LoadBalancer) getRetryCount() int {
|
|
lb.mu.RLock()
|
|
defer lb.mu.RUnlock()
|
|
return lb.retryCount
|
|
}
|
|
|
|
// resolveProviders 按用户偏好和可用性确定 provider 顺序(跳过已禁用的)。
|
|
func (lb *LoadBalancer) resolveProviders(preferred []string) []models.ProviderKind {
|
|
lb.mu.RLock()
|
|
defer lb.mu.RUnlock()
|
|
|
|
if len(preferred) == 0 {
|
|
out := make([]models.ProviderKind, 0, len(lb.pools))
|
|
for p := range lb.pools {
|
|
if !lb.disabled[p] {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
out := make([]models.ProviderKind, 0, len(preferred))
|
|
seen := make(map[models.ProviderKind]bool)
|
|
for _, name := range preferred {
|
|
p := models.ProviderKind(name)
|
|
if _, ok := lb.pools[p]; ok && !seen[p] && !lb.disabled[p] {
|
|
out = append(out, p)
|
|
seen[p] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|