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 次请求
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user