fix: 解析大画廊时连接泄漏 + 解析阶段无进度反馈

用户反馈:点开始下载后 6 个画廊(都是 2000 张级)一直显示「解析中 0/?」,
看起来像卡死。排查发现两个问题:

1. 连接泄漏(真 bug)
   httpClient() 每次请求都新建 http.Transport,用完丢弃。Transport 被丢弃后
   它持有的 keep-alive 连接不会立刻释放(要等 IdleConnTimeout=90s),
   于是每发一个请求留下一条僵尸连接。
   现场实测:91 条 Established 连接,而解析是串行的,正常应该只有 1 条。

   修复:Transport 全局复用,只在代理配置变化时重建,
   重建时 CloseIdleConnections 关掉旧连接;新增 Client.Close()。

   实测:20 次请求从 20 条连接降到 1 条;100 次并发请求只用 22 条。

2. 解析阶段毫无反馈
   2000 张 = 100 屏串行翻页,整个解析阶段界面都显示「解析中 0/?」,
   完全没有进度,用户只能干等。

   修复:FetchGalleryPage 增加 ParseProgress 回调,每翻完一屏上报一次
   (已解析张数/总张数/当前屏),界面显示「已解析 340/2000 张」。

另外:用户设置里请求间隔是 1000ms(1 秒),全局限流让 5 个画廊共享
1 请求/秒 —— 500 次解析请求需要约 8 分钟。这是配置导致的慢,不是 bug,
但可以在界面上看到进度后就不会以为卡死了。

测试从 74 增至 80,新增:
- TestTransportIsReused / TestTransportReusedUnderConcurrency(连接复用)
- TestSetProxyRebuildsTransport(改代理要重建)
- TestParseProgressCallback(进度回调,含最后一屏)
This commit is contained in:
2026-10-09 10:02:50 +08:00
parent 0623c55407
commit 7243f73d33
5 changed files with 367 additions and 25 deletions
+24 -2
View File
@@ -273,6 +273,22 @@ type existingGallery struct {
manifest *Manifest
}
// reportParseProgress 解析阶段进度反馈。
//
// 解析大画廊要翻上百屏(每屏 20 张),全程可能好几分钟。
// 界面必须看得到在动,否则「解析中 0/?」会被当成卡死。
func (e *Engine) reportParseProgress(task Task, done, total, page int) {
task.Status = "parsing"
task.TotalPages = total
task.Done = 0
// 解析期用「已解析张数」当进度,下载阶段才是真正的完成数
task.Pages = map[string]string{}
if done > 0 {
task.Pages["_parsing"] = strconv.Itoa(done)
}
e.save(task)
}
// findExistingGallery 在下载根目录里找这个 gid 已存在的画廊。
//
// 两种匹配方式:
@@ -366,7 +382,12 @@ func (e *Engine) runTask(ctx context.Context, task Task) {
// 没有可用清单(或清单太旧)→ 正常解析画廊页
if gallery == nil {
g, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token)
g, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token,
func(done, total, page int) {
// 解析阶段也要有反馈,否则大画廊(上千张 = 上百屏)
// 会长时间显示「解析中 0/?」,看着像卡死。
e.reportParseProgress(task, done, total, page)
})
if err != nil {
if ctx.Err() != nil {
e.markCanceled(task)
@@ -464,7 +485,8 @@ func (e *Engine) runTask(ctx context.Context, task Task) {
e.log("warn", fmt.Sprintf(
"画廊 %s:本地清单的页面路径似乎已失效,重新解析画廊页", task.GID))
if fresh, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token); err == nil {
if fresh, err := e.client.FetchGalleryPage(ctx, task.GID, task.Token,
func(done, total, page int) { e.reportParseProgress(task, done, total, page) }); err == nil {
// 用新解析结果重跑:重置页状态
st.reset(fresh)
st.scanExisting()