Files
eh-downloader/internal/eh/limiter.go
T
toor 0447875b6d feat: 双层并发 + 全局限流 + 整轮扫尾 + 画廊目录清单
原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。
按用户多年使用习惯(5 画廊 × 每画廊 8 张)改造为双层并发。

- 双层并发:画廊并发(默认5) × 页并发(默认8) = 最多 40 张同时下载,
  两个值都在设置面板可调(1-20)。页级 worker 池 + mutex 保护计数。
- 全局限流:对齐参考实现的 nextRequestTime 算法,限所有请求
  (含 /s/ 解析与图片本体),默认 0 秒关闭。
- 整轮扫尾:跑完后自动重扫失败页 1 轮(同样走完整 4 次退避重试),
  仍失败则停下并标记 partial,换线路后手动点开始即可续传。
- 画廊目录清单 .ehdl.json:记录 gid/token/总页数/已完成页,
  即使 state.json 丢失也能算出「应该多少张、还差多少张」。

顺带修复:
- Start() 双重加锁导致死锁(引擎完全无法启动)
- clampInt 越界时返回默认值而非边界值(设 30 会静默变 8)
- 直链正则写死 hath.network,CDN 换域名会全军覆没 -> 加图片 URL 兜底
- ReconcileTask 会误报「有改动」

测试从 37 增至 62,其中并发/限流/扫尾用 httptest 模拟 EH 实测:
- 页并发峰值 = 8、画廊并发峰值 = 5(完全生效)
- 12 次请求限流 80ms 实测 966ms(期望 880ms)
- 断点续传 4 张已存在时只发 5 次请求
2026-10-09 00:11:38 +08:00

87 lines
1.9 KiB
Go

package eh
// 全局限流器
//
// 算法对齐参考实现(E-Hentai Downloader 的「请求间隔」设置):
// 用一个全局的 nextRequestTime,每次请求都把自己排到它后面,再把它往后推。
//
// next = max(next, now) + interval
// 等待到 next 时刻再真正发出
//
// 效果是「所有请求之间至少间隔 interval 毫秒」,与并发数无关 ——
// 哪怕 40 个 worker 同时来,也会被串成每 interval 放行一个。
//
// interval = 0 时完全旁路,不产生任何额外开销。
import (
"context"
"sync"
"time"
)
// Limiter 全局请求节流器
type Limiter struct {
mu sync.Mutex
interval time.Duration
next time.Time // 下一个请求允许发出的时刻
}
// 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
}
// Wait 占用一个请求名额,必要时阻塞到放行时刻。
// 返回 error 是为了能响应取消(点「停止」时不该继续排队)。
func (l *Limiter) Wait(ctx context.Context) error {
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()
delay := time.Until(at)
if delay <= 0 {
return ctx.Err()
}
t := time.NewTimer(delay)
defer t.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
return nil
}
}