Skip to content

Instantly share code, notes, and snippets.

@dixonwhitmire
Created June 14, 2024 02:36
Show Gist options
  • Save dixonwhitmire/a656e65b5e4f0e9f6dc1f3f8f78abcef to your computer and use it in GitHub Desktop.
Save dixonwhitmire/a656e65b5e4f0e9f6dc1f3f8f78abcef to your computer and use it in GitHub Desktop.
retry io functions in golang
// Package retry supports retrying operations supported within the [RetryOperation] type set.
package retry
import (
"fmt"
"log/slog"
"os"
"time"
)
const (
// MaxBackOffSeconds sets the upper limit on the number of seconds used to calculate the duration between retries.
MaxBackOffSeconds = 10
// MaxRetryCount sets the upper limit on retries.
MaxRetryCount = 10
)
// OpenerFunc supports opening a file returning a handle or an error.
type OpenerFunc func(filePath string) (*os.File, error)
// CsvReaderFunc supports reading a csv record from file or an error.
type CsvReaderFunc func() ([]string, error)
// CsvWriterFunc supports writing a csv record to file or returning an error.
type CsvWriterFunc func([]string) error
// RetryableConstraint is used to constrain the types of supported retryable operations.
type RetryableConstraint interface {
OpenerFunc | CsvReaderFunc | CsvWriterFunc
}
// Try executes operation `t` with its argument, `operationArg` enabling retries up to `retryCount` times.
// The duration between retry attempts is calculated using the `currentCount` * `retryBackOffSeconds`.
// `retryCount` defaults to 1 if it set to 0 and is capped at 10 times.
// `retryBackOffSeconds` is capped to 10 seconds
func Try[T RetryableConstraint](t T, operationArg any, retryCount uint, retryBackOffSeconds uint) (any, error) {
// set our reasonable defaults/limits
if retryCount == 0 {
retryCount = 1
}
if retryCount > 10 {
retryCount = MaxRetryCount
}
if retryBackOffSeconds > 10 {
retryBackOffSeconds = MaxBackOffSeconds
}
var err error
var funcReturn any
var warningLogMessage string
// use 1 based indexed to align with the provided retry count
for currentCount := uint(1); currentCount <= retryCount; currentCount++ {
switch retryFunc := any(t).(type) {
case OpenerFunc:
filePath := operationArg.(string)
funcReturn, err = retryFunc(filePath)
warningLogMessage = fmt.Sprintf("error opening file %s", filePath)
case CsvReaderFunc:
funcReturn, err = retryFunc()
warningLogMessage = "error reading from csv file"
case CsvWriterFunc:
csvRecord := operationArg.([]string)
err = retryFunc(csvRecord)
warningLogMessage = "error writing to csv file"
}
if err != nil {
logWarningAndWait(warningLogMessage, currentCount, retryCount, retryBackOffSeconds)
continue
}
break
}
return funcReturn, err
}
// logWarningAndWait emits a warning log message using the configured logger with details regarding the error, and then
// waits.
func logWarningAndWait(logMessage string, currentCount uint, retryCount uint, retryBackOffSeconds uint) {
slog.Warn(logMessage,
"current_attempt", currentCount,
"retries_remaining", retryCount-currentCount)
waitDuration := time.Duration(currentCount*retryBackOffSeconds) * time.Second
time.Sleep(waitDuration)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment