diff --git a/internal/upgrade/download.go b/internal/upgrade/download.go index 99b94bc..1eaf577 100644 --- a/internal/upgrade/download.go +++ b/internal/upgrade/download.go @@ -16,6 +16,43 @@ import ( var httpClient = &http.Client{Timeout: 120 * time.Second} +const ( + maxDownloadRetries = 3 + retryBaseDelay = 5 * time.Second +) + +// retryDelays returns the wait duration before the nth retry (1-based). +// Delays: 5s, 15s — increasing gap to avoid hammering on transient failures. +func retryDelay(attempt int) time.Duration { + return retryBaseDelay * time.Duration(attempt*attempt) +} + +// downloadWithRetry fetches the release archive, retrying on transient errors. +// onProgress is called with user-facing messages (may be nil). +func downloadWithRetry(ctx context.Context, version string, onProgress func(string)) (string, error) { + var lastErr error + for attempt := 1; attempt <= maxDownloadRetries; attempt++ { + path, err := download(ctx, version) + if err == nil { + return path, nil + } + lastErr = err + if attempt < maxDownloadRetries { + delay := retryDelay(attempt) + if onProgress != nil { + onProgress(fmt.Sprintf("Download failed (%v)", err)) + onProgress(fmt.Sprintf("Retrying in %s... (attempt %d/%d)", delay, attempt+1, maxDownloadRetries)) + } + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(delay): + } + } + } + return "", lastErr +} + // download fetches the release archive to a temporary file. func download(ctx context.Context, version string) (string, error) { url := releaseURL(version, archiveName(version)) diff --git a/internal/upgrade/upgrade.go b/internal/upgrade/upgrade.go index 5d31308..6a675d2 100644 --- a/internal/upgrade/upgrade.go +++ b/internal/upgrade/upgrade.go @@ -83,7 +83,7 @@ func (u *Upgrader) Execute(ctx context.Context, targetVersion string) Result { // 4. Download archive u.log(fmt.Sprintf("Downloading v%s...", targetVersion)) - archivePath, err := download(ctx, targetVersion) + archivePath, err := downloadWithRetry(ctx, targetVersion, u.log) if err != nil { return u.fail("download: %v", err) }