github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/retry/retry.go (about)

     1  package retry
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/backoff"
     8  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
     9  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/wait"
    10  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    11  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    12  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    13  )
    14  
    15  // retryOperation is the interface that holds an operation for retry.
    16  // if retryOperation returns not nil - operation will retry
    17  // if retryOperation returns nil - retry loop will break
    18  type retryOperation func(context.Context) (err error)
    19  
    20  type retryOptions struct {
    21  	label       string
    22  	call        call
    23  	trace       *trace.Retry
    24  	idempotent  bool
    25  	stackTrace  bool
    26  	fastBackoff backoff.Backoff
    27  	slowBackoff backoff.Backoff
    28  
    29  	panicCallback func(e interface{})
    30  }
    31  
    32  type Option interface {
    33  	ApplyRetryOption(opts *retryOptions)
    34  }
    35  
    36  var _ Option = labelOption("")
    37  
    38  type labelOption string
    39  
    40  func (label labelOption) ApplyDoOption(opts *doOptions) {
    41  	opts.retryOptions = append(opts.retryOptions, WithLabel(string(label)))
    42  }
    43  
    44  func (label labelOption) ApplyDoTxOption(opts *doTxOptions) {
    45  	opts.retryOptions = append(opts.retryOptions, WithLabel(string(label)))
    46  }
    47  
    48  func (label labelOption) ApplyRetryOption(opts *retryOptions) {
    49  	opts.label = string(label)
    50  }
    51  
    52  // WithLabel applies label for identification call Retry in trace.Retry.OnRetry
    53  func WithLabel(label string) labelOption {
    54  	return labelOption(label)
    55  }
    56  
    57  var _ Option = (*callOption)(nil)
    58  
    59  type callOption struct {
    60  	call
    61  }
    62  
    63  func (call callOption) ApplyDoOption(opts *doOptions) {
    64  	opts.retryOptions = append(opts.retryOptions, withCaller(call))
    65  }
    66  
    67  func (call callOption) ApplyDoTxOption(opts *doTxOptions) {
    68  	opts.retryOptions = append(opts.retryOptions, withCaller(call))
    69  }
    70  
    71  func (call callOption) ApplyRetryOption(opts *retryOptions) {
    72  	opts.call = call
    73  }
    74  
    75  type call interface {
    76  	FunctionID() string
    77  }
    78  
    79  func withCaller(call call) callOption {
    80  	return callOption{call}
    81  }
    82  
    83  var _ Option = stackTraceOption{}
    84  
    85  type stackTraceOption struct{}
    86  
    87  func (stackTraceOption) ApplyRetryOption(opts *retryOptions) {
    88  	opts.stackTrace = true
    89  }
    90  
    91  func (stackTraceOption) ApplyDoOption(opts *doOptions) {
    92  	opts.retryOptions = append(opts.retryOptions, WithStackTrace())
    93  }
    94  
    95  func (stackTraceOption) ApplyDoTxOption(opts *doTxOptions) {
    96  	opts.retryOptions = append(opts.retryOptions, WithStackTrace())
    97  }
    98  
    99  // WithStackTrace wraps errors with stacktrace from Retry call
   100  func WithStackTrace() stackTraceOption {
   101  	return stackTraceOption{}
   102  }
   103  
   104  var _ Option = traceOption{}
   105  
   106  type traceOption struct {
   107  	t *trace.Retry
   108  }
   109  
   110  func (t traceOption) ApplyRetryOption(opts *retryOptions) {
   111  	opts.trace = opts.trace.Compose(t.t)
   112  }
   113  
   114  func (t traceOption) ApplyDoOption(opts *doOptions) {
   115  	opts.retryOptions = append(opts.retryOptions, WithTrace(t.t))
   116  }
   117  
   118  func (t traceOption) ApplyDoTxOption(opts *doTxOptions) {
   119  	opts.retryOptions = append(opts.retryOptions, WithTrace(t.t))
   120  }
   121  
   122  // WithTrace returns trace option
   123  func WithTrace(t *trace.Retry) traceOption {
   124  	return traceOption{t: t}
   125  }
   126  
   127  var _ Option = idempotentOption(false)
   128  
   129  type idempotentOption bool
   130  
   131  func (idempotent idempotentOption) ApplyRetryOption(opts *retryOptions) {
   132  	opts.idempotent = bool(idempotent)
   133  }
   134  
   135  func (idempotent idempotentOption) ApplyDoOption(opts *doOptions) {
   136  	opts.retryOptions = append(opts.retryOptions, WithIdempotent(bool(idempotent)))
   137  }
   138  
   139  func (idempotent idempotentOption) ApplyDoTxOption(opts *doTxOptions) {
   140  	opts.retryOptions = append(opts.retryOptions, WithIdempotent(bool(idempotent)))
   141  }
   142  
   143  // WithIdempotent applies idempotent flag to retry operation
   144  func WithIdempotent(idempotent bool) idempotentOption {
   145  	return idempotentOption(idempotent)
   146  }
   147  
   148  var _ Option = fastBackoffOption{}
   149  
   150  type fastBackoffOption struct {
   151  	backoff backoff.Backoff
   152  }
   153  
   154  func (o fastBackoffOption) ApplyRetryOption(opts *retryOptions) {
   155  	if o.backoff != nil {
   156  		opts.fastBackoff = o.backoff
   157  	}
   158  }
   159  
   160  func (o fastBackoffOption) ApplyDoOption(opts *doOptions) {
   161  	opts.retryOptions = append(opts.retryOptions, WithFastBackoff(o.backoff))
   162  }
   163  
   164  func (o fastBackoffOption) ApplyDoTxOption(opts *doTxOptions) {
   165  	opts.retryOptions = append(opts.retryOptions, WithFastBackoff(o.backoff))
   166  }
   167  
   168  // WithFastBackoff replaces default fast backoff
   169  func WithFastBackoff(b backoff.Backoff) fastBackoffOption {
   170  	return fastBackoffOption{backoff: b}
   171  }
   172  
   173  var _ Option = slowBackoffOption{}
   174  
   175  type slowBackoffOption struct {
   176  	backoff backoff.Backoff
   177  }
   178  
   179  func (o slowBackoffOption) ApplyRetryOption(opts *retryOptions) {
   180  	if o.backoff != nil {
   181  		opts.slowBackoff = o.backoff
   182  	}
   183  }
   184  
   185  func (o slowBackoffOption) ApplyDoOption(opts *doOptions) {
   186  	opts.retryOptions = append(opts.retryOptions, WithSlowBackoff(o.backoff))
   187  }
   188  
   189  func (o slowBackoffOption) ApplyDoTxOption(opts *doTxOptions) {
   190  	opts.retryOptions = append(opts.retryOptions, WithSlowBackoff(o.backoff))
   191  }
   192  
   193  // WithSlowBackoff replaces default slow backoff
   194  func WithSlowBackoff(b backoff.Backoff) slowBackoffOption {
   195  	return slowBackoffOption{backoff: b}
   196  }
   197  
   198  var _ Option = panicCallbackOption{}
   199  
   200  type panicCallbackOption struct {
   201  	callback func(e interface{})
   202  }
   203  
   204  func (o panicCallbackOption) ApplyRetryOption(opts *retryOptions) {
   205  	opts.panicCallback = o.callback
   206  }
   207  
   208  func (o panicCallbackOption) ApplyDoOption(opts *doOptions) {
   209  	opts.retryOptions = append(opts.retryOptions, WithPanicCallback(o.callback))
   210  }
   211  
   212  func (o panicCallbackOption) ApplyDoTxOption(opts *doTxOptions) {
   213  	opts.retryOptions = append(opts.retryOptions, WithPanicCallback(o.callback))
   214  }
   215  
   216  // WithPanicCallback returns panic callback option
   217  // If not defined - panic would not intercept with driver
   218  func WithPanicCallback(panicCallback func(e interface{})) panicCallbackOption {
   219  	return panicCallbackOption{callback: panicCallback}
   220  }
   221  
   222  // Retry provide the best effort fo retrying operation
   223  //
   224  // Retry implements internal busy loop until one of the following conditions is met:
   225  //
   226  // - context was canceled or deadlined
   227  //
   228  // - retry operation returned nil as error
   229  //
   230  // Warning: if deadline without deadline or cancellation func Retry will be worked infinite
   231  //
   232  // If you need to retry your op func on some logic errors - you must return RetryableError() from retryOperation
   233  func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr error) {
   234  	options := &retryOptions{
   235  		call:        stack.FunctionID(""),
   236  		trace:       &trace.Retry{},
   237  		fastBackoff: backoff.Fast,
   238  		slowBackoff: backoff.Slow,
   239  	}
   240  	for _, opt := range opts {
   241  		if opt != nil {
   242  			opt.ApplyRetryOption(options)
   243  		}
   244  	}
   245  	if options.idempotent {
   246  		ctx = xcontext.WithIdempotent(ctx, options.idempotent)
   247  	}
   248  	defer func() {
   249  		if finalErr != nil && options.stackTrace {
   250  			finalErr = xerrors.WithStackTrace(finalErr,
   251  				xerrors.WithSkipDepth(2), // 1 - exit from defer, 1 - exit from Retry call
   252  			)
   253  		}
   254  	}()
   255  	var (
   256  		i        int
   257  		attempts int
   258  
   259  		code           = int64(0)
   260  		onIntermediate = trace.RetryOnRetry(options.trace, &ctx,
   261  			options.label, options.call, options.label, options.idempotent, xcontext.IsNestedCall(ctx),
   262  		)
   263  	)
   264  	defer func() {
   265  		onIntermediate(finalErr)(attempts, finalErr)
   266  	}()
   267  	for {
   268  		i++
   269  		attempts++
   270  		select {
   271  		case <-ctx.Done():
   272  			return xerrors.WithStackTrace(
   273  				fmt.Errorf("retry failed on attempt No.%d: %w",
   274  					attempts, ctx.Err(),
   275  				),
   276  			)
   277  
   278  		default:
   279  			err := func() (err error) {
   280  				if options.panicCallback != nil {
   281  					defer func() {
   282  						if e := recover(); e != nil {
   283  							options.panicCallback(e)
   284  							err = xerrors.WithStackTrace(
   285  								fmt.Errorf("panic recovered: %v", e),
   286  							)
   287  						}
   288  					}()
   289  				}
   290  
   291  				return op(ctx)
   292  			}()
   293  
   294  			if err == nil {
   295  				return nil
   296  			}
   297  
   298  			if ctxErr := ctx.Err(); ctxErr != nil {
   299  				return xerrors.WithStackTrace(
   300  					xerrors.Join(
   301  						fmt.Errorf("context error occurred on attempt No.%d", attempts),
   302  						ctxErr, err,
   303  					),
   304  				)
   305  			}
   306  
   307  			m := Check(err)
   308  
   309  			if m.StatusCode() != code {
   310  				i = 0
   311  			}
   312  
   313  			if !m.MustRetry(options.idempotent) {
   314  				return xerrors.WithStackTrace(
   315  					fmt.Errorf("non-retryable error occurred on attempt No.%d (idempotent=%v): %w",
   316  						attempts, options.idempotent, err,
   317  					),
   318  				)
   319  			}
   320  
   321  			if e := wait.Wait(ctx, options.fastBackoff, options.slowBackoff, m.BackoffType(), i); e != nil {
   322  				return xerrors.WithStackTrace(
   323  					xerrors.Join(
   324  						fmt.Errorf("wait exit on attempt No.%d",
   325  							attempts,
   326  						), e, err,
   327  					),
   328  				)
   329  			}
   330  
   331  			code = m.StatusCode()
   332  
   333  			onIntermediate(err)
   334  		}
   335  	}
   336  }
   337  
   338  // Check returns retry mode for queryErr.
   339  func Check(err error) (m retryMode) {
   340  	code, errType, backoffType, deleteSession := xerrors.Check(err)
   341  
   342  	return retryMode{
   343  		code:          code,
   344  		errType:       errType,
   345  		backoff:       backoffType,
   346  		deleteSession: deleteSession,
   347  	}
   348  }