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:
+65
-8
@@ -278,7 +278,8 @@ type Client struct {
|
||||
proxyMode string // off | http | socks5
|
||||
xres string // auto | org | 数字(长边上限)
|
||||
|
||||
limiter *Limiter
|
||||
transport *http.Transport // 全局共用,只有代理配置变了才重建
|
||||
limiter *Limiter
|
||||
}
|
||||
|
||||
// NewClient 建客户端
|
||||
@@ -319,6 +320,7 @@ func (c *Client) SetProxy(p, mode string) {
|
||||
c.proxyMode = mode
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.rebuildTransport() // 代理变了,Transport 必须重建
|
||||
}
|
||||
|
||||
// SetXres 改分辨率策略
|
||||
@@ -341,10 +343,32 @@ func (c *Client) snapshot() (string, string, time.Duration) {
|
||||
return c.proxy, c.proxyMode, c.timeout
|
||||
}
|
||||
|
||||
// httpClient 按当前代理配置构造 http.Client。
|
||||
// 每次调用新建(配置变更即时生效,且下载与页面请求的超时不同)。
|
||||
// httpClient 取可用的 http.Client。
|
||||
//
|
||||
// ⚠️ Transport 必须复用:如果每次请求都新建 Transport,旧的那个会被丢弃,
|
||||
// 但它持有的 keep-alive 连接不会立刻释放(要等 IdleConnTimeout),
|
||||
// 于是每请求一条僵尸连接堆积起来 —— 实测能到 91 条,
|
||||
// 既吃代理的连接配额,又让新请求变慢。
|
||||
// 所以这里只在「代理配置变化」时才重建 Transport,平时全局共用一个。
|
||||
func (c *Client) httpClient(timeout time.Duration) *http.Client {
|
||||
proxyURL, mode, defTimeout := c.snapshot()
|
||||
c.mu.RLock()
|
||||
tr := c.transport
|
||||
defTimeout := c.timeout
|
||||
c.mu.RUnlock()
|
||||
|
||||
if tr == nil {
|
||||
tr = c.rebuildTransport()
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
timeout = defTimeout
|
||||
}
|
||||
return &http.Client{Transport: tr, Timeout: timeout}
|
||||
}
|
||||
|
||||
// rebuildTransport 按当前代理配置重建 Transport,并关掉旧连接的空闲池
|
||||
func (c *Client) rebuildTransport() *http.Transport {
|
||||
proxyURL, mode, _ := c.snapshot()
|
||||
|
||||
tr := &http.Transport{
|
||||
MaxIdleConns: 64,
|
||||
@@ -374,10 +398,26 @@ func (c *Client) httpClient(timeout time.Duration) *http.Client {
|
||||
}
|
||||
}
|
||||
|
||||
if timeout <= 0 {
|
||||
timeout = defTimeout
|
||||
c.mu.Lock()
|
||||
old := c.transport
|
||||
c.transport = tr
|
||||
c.mu.Unlock()
|
||||
|
||||
if old != nil {
|
||||
old.CloseIdleConnections()
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// Close 关闭空闲连接(退出时调用)
|
||||
func (c *Client) Close() {
|
||||
c.mu.Lock()
|
||||
tr := c.transport
|
||||
c.transport = nil
|
||||
c.mu.Unlock()
|
||||
if tr != nil {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
return &http.Client{Transport: tr, Timeout: timeout}
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, u string, timeout time.Duration) (*http.Response, error) {
|
||||
@@ -419,9 +459,19 @@ func (c *Client) getText(ctx context.Context, u string) (string, error) {
|
||||
|
||||
// ---------------------------------------------------------------- 1) 画廊页
|
||||
|
||||
// ParseProgress 解析进度回调
|
||||
//
|
||||
// 大画廊要翻很多页(2000 张 = 100 页),没有进度反馈的话界面会一直显示
|
||||
// 「解析中 0/?」,看着像卡死了。
|
||||
//
|
||||
// done = 已收集到的图片数
|
||||
// total = 站点声明的总图数(第一页才知道,可能为 0)
|
||||
// page = 当前翻到第几屏(每屏 20 张)
|
||||
type ParseProgress func(done, total, page int)
|
||||
|
||||
// FetchGalleryPage 取画廊页。注意:EH 每屏只列 20 张缩略图,
|
||||
// 必须翻 ?p=N 逐屏收集,否则静默只下到前 20 张且不报错。
|
||||
func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string) (*Gallery, error) {
|
||||
func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string, onProgress ParseProgress) (*Gallery, error) {
|
||||
var (
|
||||
pages []GalleryPageInfo
|
||||
seen = map[int]bool{}
|
||||
@@ -486,6 +536,13 @@ func (c *Client) FetchGalleryPage(ctx context.Context, gid, token string) (*Gall
|
||||
if added == 0 {
|
||||
break // 这屏没新东西 -> 到头了
|
||||
}
|
||||
|
||||
// 报告进度:大画廊翻几十上百屏时,界面上要看得见在动
|
||||
// (放在 break 之前,保证最后一屏也会报告)
|
||||
if onProgress != nil {
|
||||
onProgress(len(pages), total, p+1)
|
||||
}
|
||||
|
||||
if total > 0 && len(pages) >= total {
|
||||
break // 收齐
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user