package hd4k import ( "context" "sync" "time" ) // 全局限流器 // // 算法对齐参考实现(E-Hentai Downloader 的「请求间隔」设置): // 用一个全局的 nextRequestTime,每次请求都把自己排到它后面,再把它往后推。 // // next = max(next, now) + interval // 等待到 next 时刻再真正发出 // // 效果是「所有请求之间至少间隔 interval 毫秒」,与并发数无关 —— // 哪怕 40 个 worker 同时来,也会被串成每 interval 放行一个。 // // interval = 0 时完全旁路,不产生任何额外开销。 // Limiter 全局请求节流器 type Limiter struct { mu sync.Mutex interval time.Duration next time.Time // 下一个请求允许发出的时刻 until time.Time // 全局冷却截止时刻(429 时设置) } // NewLimiter 建限流器(interval <= 0 表示不限流) func NewLimiter(interval time.Duration) *Limiter { if interval < 0 { interval = 0 } return &Limiter{interval: interval} } // SetInterval 运行时改间隔 func (l *Limiter) SetInterval(d time.Duration) { if d < 0 { d = 0 } l.mu.Lock() l.interval = d l.mu.Unlock() } // Interval 当前间隔 func (l *Limiter) Interval() time.Duration { l.mu.Lock() defer l.mu.Unlock() return l.interval } // Cooldown 触发全局冷却:所有在途和后续请求一起等 d。 // // 这是 429 的正确应对方式。当前实现里 40 个 worker 各自独立退避, // 撞到 429 后它们会错峰重试 —— 结果是持续不断往已经限流的服务器上 // 打请求,永远缓不过来。全局冷却让**所有** worker 一起停下, // 冷却期满再一起放行,这才是真能让对方缓过来的节奏。 // // 多次触发取最晚时刻,避免短时间连环 429 把冷却不断重置到更短。 func (l *Limiter) Cooldown(d time.Duration) { if d <= 0 { return } l.mu.Lock() defer l.mu.Unlock() if until := time.Now().Add(d); until.After(l.until) { l.until = until } } // CooldownLeft 当前剩余的全局冷却时长(0 = 没有冷却) func (l *Limiter) CooldownLeft() time.Duration { l.mu.Lock() defer l.mu.Unlock() if d := time.Until(l.until); d > 0 { return d } return 0 } // Wait 占用一个请求名额,必要时阻塞到放行时刻。 // 返回 error 是为了能响应取消(点「停止」时不该继续排队)。 // // ⚠️ 全局冷却(Cooldown)必须在 interval 旁路**之前**判断: // 默认 interval=0 会直接 return,不检查冷却的话 Cooldown 就是死代码。 func (l *Limiter) Wait(ctx context.Context) error { // 先等全局冷却结束 for { l.mu.Lock() cooldown := time.Until(l.until) l.mu.Unlock() if cooldown <= 0 { break } // 分片等,期间可能有新的更长冷却被设置进来 if cooldown > 300*time.Millisecond { cooldown = 300 * time.Millisecond } if err := sleepCtx(ctx, cooldown); err != nil { return err } } l.mu.Lock() interval := l.interval if interval <= 0 { l.mu.Unlock() return ctx.Err() } now := time.Now() // 关键:如果过去的排队已经堆积,next 可能远在未来, // 这里取 max 让第一个请求立刻发出去,避免冷启动时集体等一大轮。 if l.next.Before(now) { l.next = now } at := l.next l.next = at.Add(interval) l.mu.Unlock() return sleepCtx(ctx, time.Until(at)) } // sleepCtx 睡到 d 后返回,期间被取消则返回 ctx.Err() func sleepCtx(ctx context.Context, d time.Duration) error { if d <= 0 { return ctx.Err() } t := time.NewTimer(d) defer t.Stop() select { case <-ctx.Done(): return ctx.Err() case <-t.C: return nil } }