github.com/diadata-org/diadata@v1.4.593/pkg/dia/helpers/substrate-helper/gsrpc/registry/exec/exec.go (about)

     1  package exec
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  )
     9  
    10  //go:generate mockery --name RetryableExecutor --structname RetryableExecutorMock --filename exec_mock.go --inpackage
    11  
    12  // RetryableExecutor is the interface used for executing a closure and its fallback if the initial execution fails.
    13  //
    14  // The interface is generic over type T which represents the return value of the closure.
    15  type RetryableExecutor[T any] interface {
    16  	ExecWithFallback(execFn func() (T, error), fallbackFn func() error) (T, error)
    17  }
    18  
    19  // retryableExecutor implements RetryableExecutor.
    20  //
    21  // It can be configured via the provided OptsFn(s).
    22  type retryableExecutor[T any] struct {
    23  	opts *Opts
    24  }
    25  
    26  // NewRetryableExecutor creates a new RetryableExecutor.
    27  func NewRetryableExecutor[T any](opts ...OptsFn) RetryableExecutor[T] {
    28  	execOpts := NewDefaultExecOpts()
    29  
    30  	for _, opt := range opts {
    31  		opt(execOpts)
    32  	}
    33  
    34  	return &retryableExecutor[T]{
    35  		execOpts,
    36  	}
    37  }
    38  
    39  // ExecWithFallback will attempt to execute the provided execFn and, in the case of failure, it will execute
    40  // the fallbackFn and retry execution of execFn.
    41  func (r *retryableExecutor[T]) ExecWithFallback(execFn func() (T, error), fallbackFn func() error) (res T, err error) {
    42  	if execFn == nil {
    43  		return res, ErrMissingExecFn
    44  	}
    45  
    46  	if fallbackFn == nil {
    47  		return res, ErrMissingFallbackFn
    48  	}
    49  
    50  	execErr := &Error{}
    51  
    52  	retryCount := uint(0)
    53  
    54  	for {
    55  		res, err = execFn()
    56  
    57  		if err == nil {
    58  			return res, nil
    59  		}
    60  
    61  		execErr.AddErr(fmt.Errorf("exec function error: %w", err))
    62  
    63  		if retryCount == r.opts.maxRetryCount {
    64  			return res, execErr
    65  		}
    66  
    67  		if err = fallbackFn(); err != nil && !r.opts.retryOnFallbackError {
    68  			execErr.AddErr(fmt.Errorf("fallback function error: %w", err))
    69  
    70  			return res, execErr
    71  		}
    72  
    73  		retryCount++
    74  
    75  		time.Sleep(r.opts.retryTimeout)
    76  	}
    77  }
    78  
    79  var (
    80  	ErrMissingExecFn     = errors.New("no exec function provided")
    81  	ErrMissingFallbackFn = errors.New("no fallback function provided")
    82  )
    83  
    84  const (
    85  	defaultMaxRetryCount        = 3
    86  	defaultErrTimeout           = 0 * time.Second
    87  	defaultRetryOnFallbackError = true
    88  )
    89  
    90  // Opts holds the configurable options for a RetryableExecutor.
    91  type Opts struct {
    92  	// maxRetryCount holds maximum number of retries in the case of failure.
    93  	maxRetryCount uint
    94  
    95  	// retryTimeout holds the timeout between retries.
    96  	retryTimeout time.Duration
    97  
    98  	// retryOnFallbackError specifies whether a retry will be done in the case of
    99  	// failure of the fallback function.
   100  	retryOnFallbackError bool
   101  }
   102  
   103  // NewDefaultExecOpts creates the default Opts.
   104  func NewDefaultExecOpts() *Opts {
   105  	return &Opts{
   106  		maxRetryCount:        defaultMaxRetryCount,
   107  		retryTimeout:         defaultErrTimeout,
   108  		retryOnFallbackError: defaultRetryOnFallbackError,
   109  	}
   110  }
   111  
   112  // OptsFn is function that operate on Opts.
   113  type OptsFn func(opts *Opts)
   114  
   115  // WithMaxRetryCount sets the max retry count.
   116  //
   117  // Note that a default value is provided if the provided count is 0.
   118  func WithMaxRetryCount(maxRetryCount uint) OptsFn {
   119  	return func(opts *Opts) {
   120  		if maxRetryCount == 0 {
   121  			maxRetryCount = defaultMaxRetryCount
   122  		}
   123  
   124  		opts.maxRetryCount = maxRetryCount
   125  	}
   126  }
   127  
   128  // WithRetryTimeout sets the retry timeout.
   129  func WithRetryTimeout(retryTimeout time.Duration) OptsFn {
   130  	return func(opts *Opts) {
   131  		opts.retryTimeout = retryTimeout
   132  	}
   133  }
   134  
   135  // WithRetryOnFallBackError sets the retryOnFallbackError flag.
   136  func WithRetryOnFallBackError(retryOnFallbackError bool) OptsFn {
   137  	return func(opts *Opts) {
   138  		opts.retryOnFallbackError = retryOnFallbackError
   139  	}
   140  }
   141  
   142  // Error holds none or multiple errors that can happen during execution.
   143  type Error struct {
   144  	errs []error
   145  }
   146  
   147  // AddErr appends an error to the error slice of Error.
   148  func (e *Error) AddErr(err error) {
   149  	e.errs = append(e.errs, err)
   150  }
   151  
   152  // Error implements the standard error interface.
   153  func (e *Error) Error() string {
   154  	sb := strings.Builder{}
   155  
   156  	for i, err := range e.errs {
   157  		sb.WriteString(fmt.Sprintf("error %d: %s\n", i, err))
   158  	}
   159  
   160  	return sb.String()
   161  }