github.com/blend/go-sdk@v1.20220411.3/retry/retry.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package retry
     9  
    10  import (
    11  	"context"
    12  	"time"
    13  
    14  	"github.com/blend/go-sdk/async"
    15  	"github.com/blend/go-sdk/ex"
    16  )
    17  
    18  var (
    19  	_ async.Interceptor = (*Retrier)(nil)
    20  )
    21  
    22  // Retry calls an actioner with retries.
    23  func Retry(ctx context.Context, action Actioner, args interface{}, opts ...Option) (interface{}, error) {
    24  	return New(opts...).Intercept(action).Action(ctx, args)
    25  }
    26  
    27  // New wraps an actioner with retries.
    28  func New(opts ...Option) *Retrier {
    29  	retrier := Retrier{}
    30  	Defaults(&retrier)
    31  	for _, opt := range opts {
    32  		opt(&retrier)
    33  	}
    34  	return &retrier
    35  }
    36  
    37  // Retrier is the retry agent.
    38  type Retrier struct {
    39  	MaxAttempts         uint
    40  	DelayProvider       DelayProvider
    41  	ShouldRetryProvider ShouldRetryProvider
    42  }
    43  
    44  // Intercept calls a function and retries on error or if a should retry provider
    45  // is set, based on the should retry result.
    46  func (r Retrier) Intercept(action Actioner) Actioner {
    47  	return ActionerFunc(func(ctx context.Context, args interface{}) (res interface{}, err error) {
    48  		var attempt uint
    49  		var alarm *time.Timer
    50  
    51  		// treat maxAttempts == 0 as mostly unbounded
    52  		// we have to keep attempts for the delay provider
    53  		for attempt = 0; (r.MaxAttempts) == 0 || (attempt < r.MaxAttempts); attempt++ {
    54  			func() {
    55  				defer func() {
    56  					if r := recover(); r != nil {
    57  						err = ex.New(r)
    58  					}
    59  				}()
    60  				res, err = action.Action(ctx, args)
    61  			}()
    62  			if err == nil {
    63  				return
    64  			}
    65  			if !r.ShouldRetryProvider(err) {
    66  				return
    67  			}
    68  
    69  			// use a (somewhat) persistent alarm reference
    70  			alarm = time.NewTimer(r.DelayProvider(ctx, attempt))
    71  			select {
    72  			case <-ctx.Done():
    73  				alarm.Stop()
    74  				err = context.Canceled
    75  				return
    76  			case <-alarm.C:
    77  				alarm.Stop()
    78  			}
    79  		}
    80  		return
    81  	})
    82  }