diff --git a/publisher/download/download.go b/publisher/download/download.go index 2f1b9ea..838b298 100644 --- a/publisher/download/download.go +++ b/publisher/download/download.go @@ -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!") @@ -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 } diff --git a/publisher/utils/utils.go b/publisher/utils/utils.go index c33f99a..0edd95a 100644 --- a/publisher/utils/utils.go +++ b/publisher/utils/utils.go @@ -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 +} diff --git a/publisher/utils/utils_test.go b/publisher/utils/utils_test.go index 09af7b6..760d533 100644 --- a/publisher/utils/utils_test.go +++ b/publisher/utils/utils_test.go @@ -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" @@ -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()) @@ -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 @@ -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()) -} \ No newline at end of file +} + +// 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) +}