Skip to content

[Bug] 部署层所有 HTTP 上传完全没有重试机制,网络抖动即整盘失败 #46

@Tespera

Description

@Tespera

Gridea Pro 版本

main 分支 HEAD (b84db26)

操作系统

macOS (Apple Silicon)

系统版本

Darwin 25.4.0(问题与平台无关)

优先级

P1(可靠性)

Bug 描述

检查所有基于 HTTP 的 provider / service:

  • backend/internal/deploy/vercel_deployer.gouploadSingleFile / createDeployment
  • backend/internal/deploy/netlify_deployer.gouploadSingleFile / createDeploy
  • backend/internal/service/cdn_upload_service.gouploadToGitHub / getGithubFileSHA

这些函数在遇到 5xx429 Too Many Requests、短暂的 io.EOFconnection reset典型可恢复错误时,全部直接 return err 一次挂掉。errgroup 一旦拿到非 nil 错误就会取消所有其它任务,整个部署直接失败。

典型场景:

  • 移动网络切换 Wi-Fi 的一瞬间丢连接。
  • Vercel / Netlify 瞬时限流返回 429。
  • GitHub API 偶发 502 / 503(官方文档承认会有)。
  • 大文件 PUT 超过 http.Client.Timeout(60s/120s)但文件其实只是慢。

从用户视角看:"我就是网络一闪,发布就全部重来",体验很差;而实际上对幂等写(PUT、带 x-vercel-digest 的文件上传)退避几秒再试一次几乎都能成功。

此外上传 SHA 为内容哈希天然幂等,完全可以安全重试;即使重复上传到 Vercel/Netlify/GitHub 也不会产生重复资源。

复现步骤

  1. 打开 Network Link Conditioner 或类似工具,制造 30% 丢包。
  2. 触发一次 Vercel 或 Netlify 部署(文件 ≥ 50 个)。
  3. 观察:大概率中途某次 uploadSingleFile 因 TLS handshake 失败或 5xx 而整盘失败。
  4. 再次手动发布一次:同一批文件可能有一部分已经在对端 cache,效率低。

期望行为

  • 幂等的 HTTP 方法(GET、PUT、DELETE)且错误属于可重试类(网络错误、5xx、429),自动退避重试(建议 3 次,指数 + jitter)。
  • 429 要尊重 Retry-After header。
  • 不幂等方法(POST 创建资源)默认不重试;只有在请求能附带 Idempotency-Key 的场景才例外。
  • 重试过程中向前端推进度事件,告知"第 N 次重试中..."。

实际行为

无任何重试;一次失败即整盘失败。

截图 / 日志

无。

补充信息 — 最佳解决方案

封装一个通用 retry helper

backend/internal/deploy/httputil/retry.go

type RetryPolicy struct {
    MaxAttempts    int           // 默认 3
    BaseDelay      time.Duration // 默认 500ms
    MaxDelay       time.Duration // 默认 30s
    RetryableStatus []int        // 默认 408, 429, 500, 502, 503, 504
}

func DoWithRetry(ctx context.Context, client *http.Client, buildReq func() (*http.Request, error), policy RetryPolicy) (*http.Response, error) {
    var lastErr error
    for attempt := 0; attempt < policy.MaxAttempts; attempt++ {
        req, err := buildReq()
        if err != nil {
            return nil, err
        }
        resp, err := client.Do(req)
        if err == nil && !shouldRetry(resp.StatusCode, policy.RetryableStatus) {
            return resp, nil
        }
        if err == nil { resp.Body.Close() }
        lastErr = err

        delay := backoff(attempt, policy, resp) // 指数 + jitter,429 用 Retry-After
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(delay):
        }
    }
    return nil, fmt.Errorf(\"所有重试失败: %w\", lastErr)
}

关键点:

  • buildReq工厂函数,每次重试都要重新构建(尤其是 body 是 io.Reader 时 seek 不回去)。对文件上传,传入 filePath 并在工厂里 os.Open
  • shouldRetry 网络错误(net.ErrorTimeout()Temporary()) + 白名单状态码。
  • 429 若含 Retry-After 头,用其值;否则指数退避。
  • 整体尊重 ctx.Done(),支持取消。

各调用点接入

  • vercel_deployer.gouploadSingleFileDoWithRetry(PUT 幂等 + 带哈希,天然安全)。createDeployment 仅在首次返回 5xx 时重试;missing 返回不是错误不能重试。
  • netlify_deployer.gouploadSingleFile(PUT)同上;createDeploy(POST)默认不重试,除非服务端 5xx。
  • cdn_upload_service.gouploadToGitHub(PUT)接入;getGithubFileSHA(GET)接入;429 严格按 Retry-After

事件

每次重试通过 DeployEvent(参见 issue #43)上报:
{ stage: \"upload\", level: \"warn\", message: \"xxx 重试 1/3\" }

单测

  • httputest.NewServer 构造 503→503→200 的场景,验证重试 3 次成功。
  • 429 带 Retry-After: 2,验证延迟接近 2s。
  • ctx 取消中途,验证立即返回 ctx.Err()。

影响文件:

  • 新增 backend/internal/deploy/httputil/retry.go + 单测
  • backend/internal/deploy/{vercel,netlify}_deployer.go
  • backend/internal/service/cdn_upload_service.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