Skip to content

Commit

Permalink
feat: add retries to GH asset download
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenruizdegauna committed Mar 4, 2025
1 parent 4b7720c commit 88c70ab
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 10 deletions.
18 changes: 14 additions & 4 deletions publisher/download/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import (
"os"
"path"
"strings"
"time"
)

const (
urlTemplate = "https://github.com/{repo_name}/releases/download/{tag}/{src}"
urlTemplate = "https://github.com/{repo_name}/releases/download/{tag}/{src}"
retries = 5
durationAfterRetry = 2 * time.Second
)



func (d *downloader) downloadArtifact(conf config.Config, src, arch, osVersion string) error {

utils.Logger.Println("Starting downloading artifacts!")
Expand All @@ -28,7 +29,16 @@ func (d *downloader) downloadArtifact(conf config.Config, src, arch, osVersion s

utils.Logger.Println(fmt.Sprintf("[ ] Download %s into %s", url, destPath))

err := d.downloadFile(url, destPath)
err := utils.Retry(
func() error {
return d.downloadFile(url, destPath)
},
retries,
durationAfterRetry,
func() {
utils.Logger.Printf("retrying downloadFile %s\n", url)
})

if err != nil {
return err
}
Expand Down
14 changes: 14 additions & 0 deletions publisher/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,17 @@ func S3RemountFn(l *log.Logger, commandTimeout time.Duration) {
l.Printf("mounting s3 failed %v", err)
}
}

// Retry executes the provided function fn until it succeeds or the maximum number of retries is reached.
// It waits for the specified delay between each retry.
func Retry(fn func() error, retries int, delay time.Duration, onErr func()) error {
var err error
for i := 0; i < retries; i++ {
if err = fn(); err == nil {
return nil
}
onErr()
time.Sleep(delay)
}
return err
}
68 changes: 62 additions & 6 deletions publisher/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package utils

import (
"bytes"
"errors"
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"io"
"io/ioutil"
"log"
Expand Down Expand Up @@ -55,18 +58,17 @@ func reader(content string) io.ReadCloser {
return ioutil.NopCloser(bytes.NewReader([]byte(content)))
}


func Test_ExecWithRetries_Ok(t *testing.T) {
var output, outputRetry bytes.Buffer
l := log.New(&output, "", 0)
lRetry := log.New(&outputRetry, "", 0)

err := ExecLogOutput(l, "ls", time.Millisecond * 50, "/")
err := ExecLogOutput(l, "ls", time.Millisecond*50, "/")
assert.Nil(t, err)
retryCallback := func(l *log.Logger, commandTimeout time.Duration) {
l.Print("remounting")
}
err = ExecWithRetries(3, retryCallback, lRetry, "ls", time.Millisecond * 50, "/")
err = ExecWithRetries(3, retryCallback, lRetry, "ls", time.Millisecond*50, "/")
assert.Nil(t, err)

assert.Equal(t, output.String(), outputRetry.String())
Expand All @@ -78,13 +80,13 @@ func Test_ExecWithRetries_Fail(t *testing.T) {
lRetry := log.New(&outputRetry, "", 0)
retries := 3

err := ExecLogOutput(l, "ls", time.Millisecond * 50, "/non_existing_path")
err := ExecLogOutput(l, "ls", time.Millisecond*50, "/non_existing_path")
assert.Error(t, err, "exit status 1")

retryCallback := func(l *log.Logger, commandTimeout time.Duration) {
l.Print("remounting")
}
err = ExecWithRetries(retries, retryCallback, lRetry, "ls", time.Millisecond * 50, "/non_existing_path")
err = ExecWithRetries(retries, retryCallback, lRetry, "ls", time.Millisecond*50, "/non_existing_path")
assert.Error(t, err, "exit status 1")

var expectedOutput string
Expand All @@ -94,4 +96,58 @@ func Test_ExecWithRetries_Fail(t *testing.T) {
expectedOutput += fmt.Sprintf("[attempt %v] error executing command ls /non_existing_path\n", i)
}
assert.Equal(t, expectedOutput, outputRetry.String())
}
}

// A simple mock service to assert on retry functionality
type Service struct {
mock.Mock
}

func (s *Service) Do() error {
args := s.Called()
return args.Error(0)
}

func Test_RetrySuccessWithErrors(t *testing.T) {
service := &Service{}
var err = errors.New("some error")
service.On("Do", mock.Anything).Times(2).Return(err)
service.On("Do", mock.Anything).Times(1).Return(nil)

anotherService := &Service{}
anotherService.On("Do", mock.Anything).Times(2).Return(nil)

// We will execute service 3 times. First + 2 retries
err = Retry(service.Do, 5, time.Millisecond, func() { _ = anotherService.Do() })

require.NoError(t, err)
mock.AssertExpectationsForObjects(t, service, anotherService)
}

func Test_RetryNoError(t *testing.T) {
service := &Service{}
service.On("Do", mock.Anything).Times(1).Return(nil)

anotherService := &Service{}

// We will execute service 1 time. No retries
err := Retry(service.Do, 5, time.Millisecond, func() { _ = anotherService.Do() })

require.NoError(t, err)
mock.AssertExpectationsForObjects(t, service, anotherService)
}

func Test_RetryError(t *testing.T) {
service := &Service{}
var err = errors.New("some error")
service.On("Do", mock.Anything).Times(5).Return(err)

anotherService := &Service{}
anotherService.On("Do", mock.Anything).Times(5).Return(nil)

// We will execute service all the retries and error will be returned
actualErr := Retry(service.Do, 5, time.Millisecond, func() { _ = anotherService.Do() })

assert.ErrorIs(t, actualErr, err)
mock.AssertExpectationsForObjects(t, service, anotherService)
}

0 comments on commit 88c70ab

Please sign in to comment.