Files
huya-go/internal/sniper/retry.go
T
yml2213 6e5beac151 refactor(sniper): 对齐 dygou_go 调度架构,增加双时间日志并解耦预热
- 抢购调度重构:参考 dygou_go 将 sniper 拆分为独立子模块(runner/run_control/retry/exchange/classify/config/summary/category/helpers);移除全局自适应降速与轮次上限,采用账号独立循环、统一账号级退避、单请求并发闸门与首发错峰
- 预热与建连优化:默认关闭预热(避免单账号超时连坐整机开抢),开启时转为后台非阻塞协程;WebSocket 握手超时从 10s 降至 1500ms
- 双时间日志:logger 增加 SetServerOffset,校准后日志前缀自动渲染「本地时间(H虎牙估算时间)」,实现跨机器绝对毫秒对齐
- 默认配置调优:版本提升至 v0.5.0,默认线程调为 2(缓解多实例并发限流),前端自动平滑迁移
- 验证方式:go test ./... -count=1 全通,npm run build 构建正常,单测覆盖双时间与预热配置
2026-10-17 18:29:17 +08:00

188 lines
4.7 KiB
Go

package sniper
import (
"time"
)
// sessionLostLimit 返回「连续会话失效多少次后停止该账号」的阈值。
func sessionLostLimit(cfg Config) int {
if cfg.SessionLostLimit > 0 {
return cfg.SessionLostLimit
}
return defaultSessionLostLimit
}
// completeRound 每个账号每轮只更新一次连续失败/会话失效计数。
// 多目标一轮同时失败不会被放大成多次失败;任意可信业务回包都会清零会话失效。
func (s *runState) completeRound(uid int64, categories []string, sessionLimit int) {
s.mu.Lock()
defer s.mu.Unlock()
account := s.accounts[uid]
if account == nil {
return
}
hasSuccess := false
hasRetry := false
hasSessionLost := false
hasSessionAlive := false
for _, category := range categories {
switch category {
case CategorySuccess:
hasSuccess = true
hasSessionAlive = true
case CategorySessionLost:
hasSessionLost = true
default:
if sessionAliveCategory(category) {
hasSessionAlive = true
}
if backoffCategory(category) {
hasRetry = true
}
}
}
if hasSuccess {
account.failStreak = 0
account.sessionStreak = 0
return
}
if hasRetry {
account.failStreak++
}
if hasSessionAlive {
account.sessionStreak = 0
} else if hasSessionLost {
account.sessionStreak++
}
if sessionLimit > 0 && account.sessionStreak >= sessionLimit {
account.done = true
}
}
// roundDelay 返回本轮结束后该账号的统一等待时间:取本轮所有失败分类里
// 最长的退避值,不再为每个 target 单独维护下一次重试时间。
func (s *runState) roundDelay(uid int64, cfg Config, categories []string) time.Duration {
delay := cfg.Delay
if delay <= 0 {
delay = 100 * time.Millisecond
}
seen := make(map[string]bool, len(categories))
for _, category := range categories {
if category == "" || seen[category] || !backoffCategory(category) {
continue
}
seen[category] = true
if candidate := s.retryDelay(uid, cfg, category); candidate > delay {
delay = candidate
}
}
return delay
}
// retryDelay 计算当前账号在某个结局后的退避时间。
func (s *runState) retryDelay(uid int64, cfg Config, category string) time.Duration {
s.mu.Lock()
streak := 0
if account := s.accounts[uid]; account != nil {
streak = account.failStreak
}
s.mu.Unlock()
return computeRetryDelay(s.elapsed(), streak, cfg, category)
}
// computeRetryDelay 使用与 dygou_go 相同的简单阶梯:
// 分类基础值 × 时间阶段 × 连续失败倍率,最后叠加抖动并封顶 1.5 秒。
func computeRetryDelay(elapsed time.Duration, failStreak int, cfg Config, category string) time.Duration {
base := cfg.ErrorBackoff
switch category {
case CategoryTooFrequent:
base = cfg.TooFrequentBackoff
case CategoryNotOpen:
base = maxDuration(cfg.Delay, 50*time.Millisecond)
}
if base <= 0 {
base = defaultRetryBackoff
}
delay := time.Duration(stageMultiplier(elapsed, category)) * base
if mult := streakMultiplier(failStreak); mult > 1 {
delay *= time.Duration(mult)
}
if delay > maxRetryDelay {
delay = maxRetryDelay
}
delay = jitterDuration(delay)
if delay > maxRetryDelay {
delay = maxRetryDelay
}
return delay
}
// elapsed 返回相对开抢时刻的已用时,用于按阶段取退避节奏。
func (s *runState) elapsed() time.Duration {
if !s.openAt.IsZero() {
elapsed := time.Since(s.openAt)
if elapsed < 0 {
return 0
}
return elapsed
}
if s.startedAt.IsZero() {
return 0
}
return time.Since(s.startedAt)
}
// stageMultiplier 按已抢购时长返回退避倍率:开抢头 2 秒最激进,5 秒后放慢到 3 倍。
func stageMultiplier(elapsed time.Duration, category string) int {
if backoffCategory(category) {
switch {
case elapsed < 2*time.Second:
return 1
case elapsed < 5*time.Second:
return 2
default:
return 3
}
}
if elapsed >= 5*time.Second {
return 2
}
return 1
}
// backoffCategory 返回该结局是否需要账号级退避。
func backoffCategory(category string) bool {
switch category {
case "", CategorySuccess, CategoryNotEnough, CategoryAlreadyDone, CategoryOffShelf:
return false
default:
return true
}
}
// streakMultiplier 把连续失败次数换算成翻倍系数:第 3 次起 ×2,第 4 次起 ×4。
func streakMultiplier(streak int) int {
if streak <= 2 {
return 1
}
mult := 1
for i := 2; i < streak && mult < 4; i++ {
mult <<= 1
}
return mult
}
// sessionAliveCategory 返回该结局是否能证明当前账号的登录态仍然有效。
// 网络错误、协议异常和限流都不算。
func sessionAliveCategory(category string) bool {
switch category {
case CategorySuccess, CategoryNotOpen, CategorySoldOut, CategoryNotEnough,
CategoryAlreadyDone, CategoryOffShelf, CategoryCondition, CategoryFailed:
return true
default:
return false
}
}