Skip to content

[Bug] 部署流程缺少总体超时控制,卡死的 deploy 永远占用互斥锁 #49

@Tespera

Description

@Tespera

Gridea Pro 版本

main 分支 HEAD (b84db26)

操作系统

macOS (Apple Silicon)

系统版本

Darwin 25.4.0(问题与平台无关)

优先级

P1(可靠性)

Bug 描述

  • backend/internal/service/deploy_service.goDeployToRemote 并没有用 context.WithTimeout 包一层,ctx 直接取自全局 WailsContext,相当于无总超时
  • 每个 provider 的 HTTP client 虽然有自己的 timeout:
    • Vercel:Timeout: 60 * time.Secondvercel_deployer.go:57
    • Netlify:Timeout: 120 * time.Secondnetlify_deployer.go:58
    • CDN:Timeout: 60 * time.Secondcdn_upload_service.go:68
  • 但这些是每个 HTTP 请求的 timeout,不是整次 deploy 的 timeout。
  • 此外 http.Client.Timeout 对大文件上传是有害的:一个 200MB 的视频文件在 5MB/s 上行下需要 40s,网络稍抖就会触发 60s 的请求超时——请求被 client 主动 cancel,用户看到的是"上传失败"。

后果

  1. 大文件上传误杀:正常上传被 client timeout 中断。
  2. Deploy 卡死:如果进入一个非 HTTP 的阻塞点(比如 SFTP io.Copy 到一个读阻塞的网络路径),DeployService.isDeploying 持续为 true,所有后续发布请求被 ErrDeployInProgress 拒绝,直到进程重启。
  3. ctx 取消不传导:全局 WailsContext 除非应用退出否则永远不 Done,provider 内部的 select { case <-ctx.Done(): ... } 分支事实上永远不命中。

复现步骤

(硬复现较难,构造超时场景)

  1. vercel_deployer.go:223uploadSingleFile 里的 PUT,对 > 500MB 文件在慢网络下上传。
  2. 60s 后 client 主动 cancel,但传输其实还在继续——用户看到的是"部署失败",但其实只是客户端放弃。
  3. 或:构造一个 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

检查清单

  • 我已搜索过现有 Issue,确认此 Bug 尚未被报告过。
  • 我正在使用最新版本的 Gridea Pro。

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1中优先级 / 强烈建议处理bugSomething isn't workingtriage待分诊

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions