Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(可靠性)
Bug 描述
backend/internal/service/deploy_service.go 的 DeployToRemote 并没有用 context.WithTimeout 包一层,ctx 直接取自全局 WailsContext,相当于无总超时。
- 每个 provider 的 HTTP client 虽然有自己的 timeout:
- Vercel:
Timeout: 60 * time.Second(vercel_deployer.go:57)
- Netlify:
Timeout: 120 * time.Second(netlify_deployer.go:58)
- CDN:
Timeout: 60 * time.Second(cdn_upload_service.go:68)
- 但这些是每个 HTTP 请求的 timeout,不是整次 deploy 的 timeout。
- 此外
http.Client.Timeout 对大文件上传是有害的:一个 200MB 的视频文件在 5MB/s 上行下需要 40s,网络稍抖就会触发 60s 的请求超时——请求被 client 主动 cancel,用户看到的是"上传失败"。
后果:
- 大文件上传误杀:正常上传被 client timeout 中断。
- Deploy 卡死:如果进入一个非 HTTP 的阻塞点(比如 SFTP
io.Copy 到一个读阻塞的网络路径),DeployService.isDeploying 持续为 true,所有后续发布请求被 ErrDeployInProgress 拒绝,直到进程重启。
- ctx 取消不传导:全局 WailsContext 除非应用退出否则永远不 Done,provider 内部的
select { case <-ctx.Done(): ... } 分支事实上永远不命中。
复现步骤
(硬复现较难,构造超时场景)
- 在
vercel_deployer.go:223 的 uploadSingleFile 里的 PUT,对 > 500MB 文件在慢网络下上传。
- 60s 后 client 主动 cancel,但传输其实还在继续——用户看到的是"部署失败",但其实只是客户端放弃。
- 或:构造一个 SFTP 会话,在
io.Copy 进行中让服务器端 hang(TCP 不 RST 不 FIN),客户端永远阻塞。DeployToRemote 永远不返回,isDeploying 永远 true。
期望行为
- 整个
DeployToRemote 有一个总超时(默认 30 分钟,可设置),到点自动取消所有未完成操作,释放锁。
http.Client 的全局 Timeout 应拆分成连接 / TLS / ResponseHeader 级别的 timeout,body 传输本身不设单总量超时,改为 ctx 控制。
- 长时间无进展的连接通过 ctx 被主动取消。
实际行为
无总超时;HTTP 单请求 60s timeout 会误杀大文件上传;SFTP 阻塞会永远占锁。
截图 / 日志
无。
补充信息 — 最佳解决方案
1. 加总超时
在 DeployToRemote 入口:
totalTimeout := 30 * time.Minute // 未来可从 setting 读取
ctx, cancel := context.WithTimeout(ctx, totalTimeout)
defer cancel()
超时或取消时 ctx.Err() 会被 provider 感知(前提是 provider 各个阻塞点都对 ctx 敏感)。
2. 重构 HTTP client
改用拆分型 transport 配置:
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
}
return &http.Client{
// 不再设 Client.Timeout;由 ctx 控制整体
Transport: transport,
}
这样:
- 建立连接 / TLS / 收首字节有严格超时;
- 数据传输本身不受单次总量超时限制;
- 整体取消依赖
http.NewRequestWithContext(ctx, ...)。
3. ctx 敏感化
- 所有 provider 的
filepath.Walk 入口、io.Copy 大块循环 里增补 select { case <-ctx.Done(): return ctx.Err() }。
io.Copy 大块循环可以封装 ctxCopy(ctx, dst, src),每 64KB 检查一次。
- SFTP / FTP 额外启一个 goroutine 在 ctx.Done 时调用
client.Close() 打断底层连接。
4. 单测
- 测试总超时后
isDeploying 会被正确释放。
- 测试 ctx cancel 后
uploadFile 会在 1 秒内返回。
影响文件:
backend/internal/service/deploy_service.go(总超时)
backend/internal/deploy/{vercel,netlify}_deployer.go(HTTP client 改造)
backend/internal/service/cdn_upload_service.go(HTTP client 改造)
backend/internal/deploy/{sftp,ftp}_deployer.go(ctxCopy + close on cancel)
- 新增
backend/internal/deploy/httputil/ctxcopy.go
检查清单
Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(可靠性)
Bug 描述
backend/internal/service/deploy_service.go的DeployToRemote并没有用context.WithTimeout包一层,ctx 直接取自全局WailsContext,相当于无总超时。Timeout: 60 * time.Second(vercel_deployer.go:57)Timeout: 120 * time.Second(netlify_deployer.go:58)Timeout: 60 * time.Second(cdn_upload_service.go:68)http.Client.Timeout对大文件上传是有害的:一个 200MB 的视频文件在 5MB/s 上行下需要 40s,网络稍抖就会触发 60s 的请求超时——请求被 client 主动 cancel,用户看到的是"上传失败"。后果:
io.Copy到一个读阻塞的网络路径),DeployService.isDeploying持续为 true,所有后续发布请求被ErrDeployInProgress拒绝,直到进程重启。select { case <-ctx.Done(): ... }分支事实上永远不命中。复现步骤
(硬复现较难,构造超时场景)
vercel_deployer.go:223的uploadSingleFile里的 PUT,对 > 500MB 文件在慢网络下上传。io.Copy进行中让服务器端 hang(TCP 不 RST 不 FIN),客户端永远阻塞。DeployToRemote永远不返回,isDeploying 永远 true。期望行为
DeployToRemote有一个总超时(默认 30 分钟,可设置),到点自动取消所有未完成操作,释放锁。http.Client的全局Timeout应拆分成连接 / TLS / ResponseHeader 级别的 timeout,body 传输本身不设单总量超时,改为 ctx 控制。实际行为
无总超时;HTTP 单请求 60s timeout 会误杀大文件上传;SFTP 阻塞会永远占锁。
截图 / 日志
无。
补充信息 — 最佳解决方案
1. 加总超时
在
DeployToRemote入口:超时或取消时
ctx.Err()会被 provider 感知(前提是 provider 各个阻塞点都对 ctx 敏感)。2. 重构 HTTP client
改用拆分型 transport 配置:
这样:
http.NewRequestWithContext(ctx, ...)。3. ctx 敏感化
filepath.Walk入口、io.Copy大块循环 里增补select { case <-ctx.Done(): return ctx.Err() }。io.Copy大块循环可以封装ctxCopy(ctx, dst, src),每 64KB 检查一次。client.Close()打断底层连接。4. 单测
isDeploying会被正确释放。uploadFile会在 1 秒内返回。影响文件:
backend/internal/service/deploy_service.go(总超时)backend/internal/deploy/{vercel,netlify}_deployer.go(HTTP client 改造)backend/internal/service/cdn_upload_service.go(HTTP client 改造)backend/internal/deploy/{sftp,ftp}_deployer.go(ctxCopy + close on cancel)backend/internal/deploy/httputil/ctxcopy.go检查清单