原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。 按用户多年使用习惯(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 次请求
197 lines
4.8 KiB
Go
197 lines
4.8 KiB
Go
package eh
|
|
|
|
// 限流器测试:验证「所有请求之间至少间隔 N 毫秒」,
|
|
// 以及并发情况下也不会穿透(这正是对齐参考脚本的关键行为)。
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// testWriter 是 httptest 响应写的最小接口
|
|
type testWriter interface {
|
|
WriteHeader(int)
|
|
Write([]byte) (int, error)
|
|
}
|
|
|
|
func newTestServer(t *testing.T, h func(testWriter)) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
h(w)
|
|
}))
|
|
}
|
|
|
|
// interval=0 必须完全旁路,不能有任何额外延迟
|
|
func TestLimiterZeroIsNoop(t *testing.T) {
|
|
l := NewLimiter(0)
|
|
|
|
start := time.Now()
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
l.Wait(context.Background())
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
if elapsed := time.Since(start); elapsed > 200*time.Millisecond {
|
|
t.Errorf("interval=0 时不该有等待,实际耗时 %v", elapsed)
|
|
}
|
|
}
|
|
|
|
// 串行请求:每次都该等满间隔
|
|
func TestLimiterSerialSpacing(t *testing.T) {
|
|
const interval = 100 * time.Millisecond
|
|
l := NewLimiter(interval)
|
|
ctx := context.Background()
|
|
|
|
start := time.Now()
|
|
for i := 0; i < 3; i++ {
|
|
if err := l.Wait(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
elapsed := time.Since(start)
|
|
|
|
// 3 个请求 => 等 2 次间隔(第 1 个立即放行)
|
|
wantMin := 2 * interval
|
|
if elapsed < wantMin {
|
|
t.Errorf("3 个请求应至少等 %v,实际 %v(限流没生效)", wantMin, elapsed)
|
|
}
|
|
}
|
|
|
|
// 并发请求也不能穿透:N 个同时来,应该被拉成串行
|
|
func TestLimiterConcurrentSpacing(t *testing.T) {
|
|
const (
|
|
interval = 50 * time.Millisecond
|
|
n = 5
|
|
)
|
|
l := NewLimiter(interval)
|
|
ctx := context.Background()
|
|
|
|
start := time.Now()
|
|
release := make([]time.Time, n)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < n; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
if err := l.Wait(ctx); err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
release[idx] = time.Now()
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
elapsed := time.Since(start)
|
|
// n 个请求 => 至少等 (n-1) 个间隔
|
|
wantMin := time.Duration(n-1) * interval
|
|
if elapsed < wantMin {
|
|
t.Errorf("%d 个并发请求应至少耗时 %v,实际 %v(限流被穿透)", n, wantMin, elapsed)
|
|
}
|
|
}
|
|
|
|
// 取消要能立即返回,不能卡在排队里
|
|
func TestLimiterCancel(t *testing.T) {
|
|
l := NewLimiter(500 * time.Millisecond)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
// 先占掉一个名额,让后面的排在后面
|
|
l.Wait(context.Background())
|
|
|
|
go func() {
|
|
time.Sleep(50 * time.Millisecond)
|
|
cancel()
|
|
}()
|
|
|
|
start := time.Now()
|
|
err := l.Wait(ctx)
|
|
if err == nil {
|
|
t.Error("取消后 Wait 应该返回错误")
|
|
}
|
|
if elapsed := time.Since(start); elapsed > 200*time.Millisecond {
|
|
t.Errorf("取消后应立即返回,实际等��� %v", elapsed)
|
|
}
|
|
}
|
|
|
|
// 运行时改间隔要生效
|
|
func TestLimiterSetInterval(t *testing.T) {
|
|
l := NewLimiter(0)
|
|
if l.Interval() != 0 {
|
|
t.Error("初始间隔应为 0")
|
|
}
|
|
|
|
l.SetInterval(200 * time.Millisecond)
|
|
if l.Interval() != 200*time.Millisecond {
|
|
t.Errorf("改间隔失败: %v", l.Interval())
|
|
}
|
|
|
|
// 负数要被夹成 0
|
|
l.SetInterval(-100 * time.Millisecond)
|
|
if l.Interval() != 0 {
|
|
t.Errorf("负间隔应夹成 0,实际 %v", l.Interval())
|
|
}
|
|
}
|
|
|
|
// 负数构造要夹成 0
|
|
func TestNewLimiterNegative(t *testing.T) {
|
|
l := NewLimiter(-time.Second)
|
|
if l.Interval() != 0 {
|
|
t.Errorf("负间隔应夹成 0,实际 %v", l.Interval())
|
|
}
|
|
}
|
|
|
|
// Client 的限流开关
|
|
func TestClientRequestInterval(t *testing.T) {
|
|
c := NewClient(5*time.Second, "", "off", "auto")
|
|
if c.RequestInterval() != 0 {
|
|
t.Error("默认不该限流")
|
|
}
|
|
|
|
c.SetRequestInterval(150 * time.Millisecond)
|
|
if c.RequestInterval() != 150*time.Millisecond {
|
|
t.Errorf("设置失败: %v", c.RequestInterval())
|
|
}
|
|
|
|
c.SetRequestInterval(-5)
|
|
if c.RequestInterval() != 0 {
|
|
t.Errorf("负数应夹成 0,实际 %v", c.RequestInterval())
|
|
}
|
|
}
|
|
|
|
// 限流要真的作用到 HTTP 请求上(起个服务器实测)
|
|
func TestLimiterAppliesToRequests(t *testing.T) {
|
|
srv := newTestServer(t, func(w testWriter) {
|
|
w.WriteHeader(200)
|
|
w.Write([]byte("ok"))
|
|
})
|
|
defer srv.Close()
|
|
|
|
const interval = 60 * time.Millisecond
|
|
c := NewClient(5*time.Second, "", "off", "auto")
|
|
c.SetRequestInterval(interval)
|
|
|
|
start := time.Now()
|
|
for i := 0; i < 4; i++ {
|
|
if _, err := c.getText(context.Background(), srv.URL+"/x"); err != nil {
|
|
t.Fatalf("第 %d 次请求失败: %v", i+1, err)
|
|
}
|
|
}
|
|
elapsed := time.Since(start)
|
|
|
|
// 4 个请求 = 8 次 HTTP 调用?不 —— 这里只有 getText,一次一个请求,
|
|
// 但下载会额外算一次。总共 4 次请求,至少 3 个间隔。
|
|
wantMin := 3 * interval
|
|
if elapsed < wantMin {
|
|
t.Errorf("4 次请求应至少耗时 %v,实际 %v", wantMin, elapsed)
|
|
}
|
|
} |