141 lines
3.6 KiB
Go
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
|
|
}
|
|
}
|