frist
This commit is contained in:
177
internal/selector/selector.go
Normal file
177
internal/selector/selector.go
Normal file
@@ -0,0 +1,177 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user