Files
ProxyPool/internal/selector/selector.go
2026-01-31 22:53:12 +08:00

178 lines
3.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package selector
import (
"context"
"crypto/rand"
"encoding/hex"
"math/big"
mathrand "math/rand"
"net/url"
"strings"
"time"
"proxyrotator/internal/model"
"proxyrotator/internal/store"
)
// Selector 代理选择器
type Selector struct {
store store.ProxyStore
leaseTTL time.Duration
}
// NewSelector 创建选择器
func NewSelector(store store.ProxyStore, leaseTTL time.Duration) *Selector {
if leaseTTL <= 0 {
leaseTTL = 60 * time.Second
}
return &Selector{
store: store,
leaseTTL: leaseTTL,
}
}
// Next 获取下一个可用代理
func (s *Selector) Next(ctx context.Context, req model.SelectRequest) (*model.Lease, error) {
policy := req.Policy
if policy == "" {
policy = "round_robin"
}
// 查询可用代理
proxies, err := s.store.List(ctx, model.ProxyQuery{
Group: req.Group,
TagsAny: req.TagsAny,
StatusIn: []model.ProxyStatus{model.StatusAlive},
OnlyEnabled: true,
Limit: 5000,
})
if err != nil {
return nil, err
}
if len(proxies) == 0 {
return nil, model.ErrNoProxy
}
// 根据策略选择
var chosen model.Proxy
switch policy {
case "round_robin":
key := "rr:" + req.Group + ":" + normalizeSite(req.Site)
idx, err := s.store.NextIndex(ctx, key, len(proxies))
if err != nil {
return nil, err
}
chosen = proxies[idx]
case "random":
idx := mathrand.Intn(len(proxies))
chosen = proxies[idx]
case "weighted":
chosen = weightedPickByScore(proxies)
default:
return nil, model.ErrBadPolicy
}
// 创建租约
lease := model.Lease{
LeaseID: newLeaseID(),
ProxyID: chosen.ID,
Proxy: chosen,
Group: req.Group,
Site: req.Site,
ExpireAt: time.Now().Add(s.leaseTTL),
}
// 尝试保存租约(失败也可降级不存)
_ = s.store.CreateLease(ctx, lease)
return &lease, nil
}
// Report 上报使用结果
func (s *Selector) Report(ctx context.Context, leaseID, proxyID string, success bool, latencyMs int64, errText string) error {
now := time.Now()
if success {
status := model.StatusAlive
return s.store.UpdateHealth(ctx, proxyID, model.HealthPatch{
Status: &status,
ScoreDelta: 1,
SuccessInc: 1,
LatencyMs: &latencyMs,
CheckedAt: &now,
})
}
status := model.StatusDead
return s.store.UpdateHealth(ctx, proxyID, model.HealthPatch{
Status: &status,
ScoreDelta: -3,
FailInc: 1,
CheckedAt: &now,
})
}
// normalizeSite 规范化站点 URL提取域名
func normalizeSite(site string) string {
if site == "" {
return "default"
}
u, err := url.Parse(site)
if err != nil {
return site
}
host := u.Hostname()
if host == "" {
return site
}
// 去除 www 前缀
host = strings.TrimPrefix(host, "www.")
return host
}
// weightedPickByScore 按分数加权随机选择
func weightedPickByScore(proxies []model.Proxy) model.Proxy {
// 计算权重(分数 + 偏移量确保正数)
const offset = 100
totalWeight := 0
weights := make([]int, len(proxies))
for i, p := range proxies {
w := p.Score + offset
if w < 1 {
w = 1
}
weights[i] = w
totalWeight += w
}
// 随机选择
r := mathrand.Intn(totalWeight)
for i, w := range weights {
r -= w
if r < 0 {
return proxies[i]
}
}
return proxies[0]
}
// newLeaseID 生成租约 ID
func newLeaseID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
// fallback
n, _ := rand.Int(rand.Reader, big.NewInt(1<<62))
return "lease_" + n.String()
}
return "lease_" + hex.EncodeToString(b)
}