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:
@@ -20,7 +20,13 @@ func newTestStore(t *testing.T) (*Store, string) {
|
||||
func TestStoreDefaults(t *testing.T) {
|
||||
s, _ := newTestStore(t)
|
||||
st := s.Settings()
|
||||
if st.Concurrency != 3 || st.Timeout != 30000 || st.BridgePort != 37777 {
|
||||
if st.GalleryConcurrency != 5 || st.PageConcurrency != 8 {
|
||||
t.Errorf("并发默认值不对: 画廊%d 页%d", st.GalleryConcurrency, st.PageConcurrency)
|
||||
}
|
||||
if st.RequestIntervalMs != 0 {
|
||||
t.Errorf("请求间隔默认应为 0(不限流),实际 %d", st.RequestIntervalMs)
|
||||
}
|
||||
if st.Timeout != 30000 || st.BridgePort != 37777 {
|
||||
t.Errorf("默认值不对: %+v", st)
|
||||
}
|
||||
if st.DirTemplate != "{title}" {
|
||||
@@ -34,13 +40,24 @@ func TestStoreDefaults(t *testing.T) {
|
||||
// 原子写 + 重新加载
|
||||
func TestStorePersist(t *testing.T) {
|
||||
s, path := newTestStore(t)
|
||||
s.UpdateSettings(map[string]interface{}{"concurrency": 5, "proxy": "http://127.0.0.1:7890"})
|
||||
s.UpdateSettings(map[string]interface{}{
|
||||
"galleryConcurrency": 6,
|
||||
"pageConcurrency": 10,
|
||||
"requestIntervalMs": 250,
|
||||
"proxy": "http://127.0.0.1:7890",
|
||||
})
|
||||
s.Flush()
|
||||
|
||||
s2 := NewStore(path)
|
||||
got := s2.Settings()
|
||||
if got.Concurrency != 5 {
|
||||
t.Errorf("并发没存住: %d", got.Concurrency)
|
||||
if got.GalleryConcurrency != 6 {
|
||||
t.Errorf("画廊并发没存住: %d", got.GalleryConcurrency)
|
||||
}
|
||||
if got.PageConcurrency != 10 {
|
||||
t.Errorf("页并发没存住: %d", got.PageConcurrency)
|
||||
}
|
||||
if got.RequestIntervalMs != 250 {
|
||||
t.Errorf("请求间隔没存住: %d", got.RequestIntervalMs)
|
||||
}
|
||||
if got.Proxy != "http://127.0.0.1:7890" {
|
||||
t.Errorf("代理没存住: %q", got.Proxy)
|
||||
@@ -172,7 +189,7 @@ func TestStoreCorruptedFile(t *testing.T) {
|
||||
os.WriteFile(p, []byte("{ 这不是合法 JSON"), 0o644)
|
||||
|
||||
s := NewStore(p)
|
||||
if s.Settings().Concurrency != 3 {
|
||||
if s.Settings().GalleryConcurrency != 5 {
|
||||
t.Error("损坏文件应回落到默认设置")
|
||||
}
|
||||
if len(s.Tasks()) != 0 {
|
||||
@@ -221,13 +238,75 @@ func TestEngineResumeSkipsExisting(t *testing.T) {
|
||||
func TestEngineConcurrencyClamp(t *testing.T) {
|
||||
s, _ := newTestStore(t)
|
||||
e := NewEngine(s, nil, s.Settings())
|
||||
e.SetConcurrency(99)
|
||||
if e.concurrency != 8 {
|
||||
t.Errorf("并发应被限制到 8,实际 %d", e.concurrency)
|
||||
|
||||
// 默认
|
||||
if e.GalleryConcurrency() != 5 {
|
||||
t.Errorf("默认画廊并发应为 5,实际 %d", e.GalleryConcurrency())
|
||||
}
|
||||
e.SetConcurrency(0)
|
||||
if e.concurrency != 1 {
|
||||
t.Errorf("并发应被限制到 1,实际 %d", e.concurrency)
|
||||
if e.PageConcurrency() != 8 {
|
||||
t.Errorf("默认页并发应为 8,实际 %d", e.PageConcurrency())
|
||||
}
|
||||
|
||||
// 越界要夹到合理范围而不是 panic
|
||||
e.SetGalleryConcurrency(99)
|
||||
if e.GalleryConcurrency() != 20 {
|
||||
t.Errorf("画廊并发应夹到 20,实际 %d", e.GalleryConcurrency())
|
||||
}
|
||||
e.SetGalleryConcurrency(0)
|
||||
if e.GalleryConcurrency() != 5 {
|
||||
t.Errorf("画廊并发 0 应回落默认 5,实际 %d", e.GalleryConcurrency())
|
||||
}
|
||||
|
||||
e.SetPageConcurrency(99)
|
||||
if e.PageConcurrency() != 20 {
|
||||
t.Errorf("页并发应夹到 20,实际 %d", e.PageConcurrency())
|
||||
}
|
||||
e.SetPageConcurrency(-1)
|
||||
if e.PageConcurrency() != 8 {
|
||||
t.Errorf("页并发 -1 应回落默认 8,实际 %d", e.PageConcurrency())
|
||||
}
|
||||
|
||||
// 正常范围
|
||||
e.SetGalleryConcurrency(5)
|
||||
e.SetPageConcurrency(8)
|
||||
if e.GalleryConcurrency()*e.PageConcurrency() != 40 {
|
||||
t.Errorf("理论总并发应为 40,实际 %d", e.GalleryConcurrency()*e.PageConcurrency())
|
||||
}
|
||||
}
|
||||
|
||||
// 旧配置文件(只有 concurrency 字段)要能平滑迁移
|
||||
func TestSettingsMigration(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "state.json")
|
||||
// 模拟旧版 state.json:只有 concurrency,没有新的三个字段
|
||||
old := `{
|
||||
"settings": {
|
||||
"downloadRoot": "D:/dl",
|
||||
"proxy": "http://127.0.0.1:7890",
|
||||
"proxyMode": "http",
|
||||
"concurrency": 4,
|
||||
"timeout": 45000,
|
||||
"xres": "auto",
|
||||
"bridgePort": 37777,
|
||||
"dirTemplate": "{title}"
|
||||
},
|
||||
"tasks": []
|
||||
}`
|
||||
os.WriteFile(p, []byte(old), 0o644)
|
||||
|
||||
s := NewStore(p)
|
||||
got := s.Settings()
|
||||
if got.GalleryConcurrency != 5 {
|
||||
t.Errorf("旧配置迁移后画廊并发应为默认 5,实际 %d", got.GalleryConcurrency)
|
||||
}
|
||||
if got.PageConcurrency != 8 {
|
||||
t.Errorf("旧配置迁移后页并发应为默认 8,实际 %d", got.PageConcurrency)
|
||||
}
|
||||
if got.Proxy != "http://127.0.0.1:7890" {
|
||||
t.Errorf("旧配置的代理丢了: %q", got.Proxy)
|
||||
}
|
||||
if got.Timeout != 45000 {
|
||||
t.Errorf("旧配置的超时丢了: %d", got.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user