Files
2026-10-11 12:56:38 +08:00

141 lines
3.6 KiB
Go

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
}
}