github.com/mailgun/holster/v4@v4.20.0/retry/retry.go (about)

     1  package retry
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/mailgun/holster/v4/errors"
    10  	"github.com/mailgun/holster/v4/syncutil"
    11  )
    12  
    13  const (
    14  	Cancelled         = cancelReason("context cancelled")
    15  	Stopped           = cancelReason("retry stopped")
    16  	AttemptsExhausted = cancelReason("attempts exhausted")
    17  )
    18  
    19  type Func func(context.Context, int) error
    20  
    21  type cancelReason string
    22  
    23  type stopErr struct {
    24  	err error
    25  }
    26  
    27  func (e *stopErr) Error() string {
    28  	return fmt.Sprintf("stop err: %s", e.err.Error())
    29  }
    30  
    31  type Err struct {
    32  	Err      error
    33  	Reason   cancelReason
    34  	Attempts int
    35  }
    36  
    37  func (e *Err) Cause() error { return e.Err }
    38  func (e *Err) Error() string {
    39  	return fmt.Sprintf("on attempt '%d'; %s: %s", e.Attempts, e.Reason, e.Err.Error())
    40  }
    41  
    42  func (e *Err) Is(target error) bool {
    43  	_, ok := target.(*Err)
    44  	return ok
    45  }
    46  
    47  // Stop forces the retry to cancel with the provided error
    48  // and retry.Err.Reason == retry.Stopped
    49  func Stop(err error) error {
    50  	return &stopErr{err: err}
    51  }
    52  
    53  // Until will retry the provided `retry.Func` until it returns nil or
    54  // the context is cancelled. Optionally users may use `retry.Stop()` to force
    55  // the retry to terminate with an error. Returns a `retry.Err` with
    56  // the included Reason and Attempts
    57  func Until(ctx context.Context, backOff BackOff, f Func) error {
    58  	var attempt int
    59  	for {
    60  		attempt++
    61  		if err := f(ctx, attempt); err != nil {
    62  			var stop *stopErr
    63  			if errors.As(err, &stop) {
    64  				return &Err{Attempts: attempt, Reason: Stopped, Err: stop.err}
    65  			}
    66  			interval, retry := backOff.Next()
    67  			if !retry {
    68  				return &Err{Attempts: attempt, Reason: AttemptsExhausted, Err: err}
    69  			}
    70  			timer := time.NewTimer(interval)
    71  			select {
    72  			case <-timer.C:
    73  				timer.Stop()
    74  				continue
    75  			case <-ctx.Done():
    76  				if !timer.Stop() {
    77  					<-timer.C
    78  				}
    79  				return &Err{Attempts: attempt, Reason: Cancelled, Err: err}
    80  			}
    81  		}
    82  		return nil
    83  	}
    84  }
    85  
    86  type AsyncItem struct {
    87  	Retrying bool
    88  	Attempts int
    89  	Err      error
    90  }
    91  
    92  func (s *AsyncItem) Error() string {
    93  	return s.Err.Error()
    94  }
    95  
    96  type Async struct {
    97  	asyncs map[interface{}]AsyncItem
    98  	mutex  *sync.Mutex
    99  	ctx    context.Context
   100  	wg     syncutil.WaitGroup
   101  }
   102  
   103  // Given a function that takes a context, run the provided function; if it fails, retry the function asynchronously
   104  // and return Async{}. Subsequent calls to with the same 'key' will return Async{} if the function is still
   105  // retrying, this continues until the retry period has exhausted or the context expires or is cancelled.
   106  // Then the final error returned by f() is returned to the caller on the final call with the same 'key'
   107  //
   108  // The code assumes the caller will continue to call `Async()` until either the retries have exhausted or
   109  // an Async{Retrying: false} is returned.
   110  func NewRetryAsync() *Async {
   111  	return &Async{
   112  		mutex:  &sync.Mutex{},
   113  		asyncs: make(map[interface{}]AsyncItem),
   114  	}
   115  }
   116  
   117  // Return the number of active async retries
   118  func (s *Async) Len() int {
   119  	s.mutex.Lock()
   120  	defer s.mutex.Unlock()
   121  	return len(s.asyncs)
   122  }
   123  
   124  // Stop forces stop of all running async retries
   125  func (s *Async) Stop() {
   126  	s.wg.Stop()
   127  }
   128  
   129  // Wait waits for all running async retries to complete
   130  func (s *Async) Wait() {
   131  	s.wg.Wait()
   132  }
   133  
   134  func (s *Async) Async(key interface{}, ctx context.Context, bo BackOff,
   135  	f func(context.Context, int) error) *AsyncItem {
   136  
   137  	// does this key have an existing retry running?
   138  	s.mutex.Lock()
   139  	if async, ok := s.asyncs[key]; ok {
   140  		// Remove entries that are no longer re-trying
   141  		if !async.Retrying {
   142  			delete(s.asyncs, key)
   143  		}
   144  		s.mutex.Unlock()
   145  		return &async
   146  	}
   147  	s.mutex.Unlock()
   148  
   149  	// Attempt to run the function, if successful return nil
   150  	err := f(s.ctx, 0)
   151  	if err == nil {
   152  		return nil
   153  	}
   154  
   155  	async := AsyncItem{
   156  		Retrying: true,
   157  		Err:      err,
   158  	}
   159  
   160  	s.mutex.Lock()
   161  	s.asyncs[key] = async
   162  	s.mutex.Unlock()
   163  
   164  	// Create an go routine to run the retry
   165  	s.wg.Until(func(done chan struct{}) bool {
   166  		async := AsyncItem{Retrying: true}
   167  
   168  		for {
   169  			// Retry the function
   170  			async.Attempts++
   171  			async.Err = f(ctx, async.Attempts)
   172  
   173  			// If success, then indicate we are no longer retrying
   174  			if async.Err == nil {
   175  				async.Retrying = false
   176  
   177  				s.mutex.Lock()
   178  				s.asyncs[key] = async
   179  				s.mutex.Unlock()
   180  				return false
   181  			}
   182  
   183  			// Record the error and attempts
   184  			s.mutex.Lock()
   185  			s.asyncs[key] = async
   186  			s.mutex.Unlock()
   187  
   188  			interval, retry := bo.Next()
   189  			if !retry {
   190  				async.Retrying = false
   191  				s.mutex.Lock()
   192  				s.asyncs[key] = async
   193  				s.mutex.Unlock()
   194  				return false
   195  			}
   196  
   197  			timer := time.NewTimer(interval)
   198  			select {
   199  			case <-timer.C:
   200  				timer.Stop()
   201  			case <-ctx.Done():
   202  				async.Retrying = false
   203  
   204  				s.mutex.Lock()
   205  				s.asyncs[key] = async
   206  				s.mutex.Unlock()
   207  				timer.Stop()
   208  				return false
   209  			case <-done:
   210  				// immediate abort, abandon all work
   211  				if !timer.Stop() {
   212  					<-timer.C
   213  				}
   214  				return false
   215  			}
   216  		}
   217  	})
   218  	return &async
   219  }
   220  
   221  // Return errors from failed asyncs and clean up the internal async map
   222  func (s *Async) Errs() map[interface{}]AsyncItem {
   223  	results := make(map[interface{}]AsyncItem)
   224  	s.mutex.Lock()
   225  
   226  	for key, async := range s.asyncs {
   227  		// Remove entries that are no longer re-trying
   228  		if !async.Retrying {
   229  			delete(s.asyncs, key)
   230  
   231  			// Only include async's that had an error
   232  			if async.Err != nil {
   233  				results[key] = async
   234  			}
   235  		}
   236  	}
   237  	s.mutex.Unlock()
   238  	return results
   239  }