fix(upgrade): retry download on transient network errors with user feedback

Add downloadWithRetry with up to 3 attempts and quadratic backoff (5s, 20s)
to handle TLS timeouts and transient failures. Progress messages inform the
user of each failure and wait time before retrying.
This commit is contained in:
Deivid Soto 2026-04-09 14:15:32 +02:00
parent 29f4886a53
commit db3e74a736
2 changed files with 38 additions and 1 deletions

View file

@ -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))

View file

@ -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)
}