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

     1  package retry
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/backoff"
     9  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
    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/retry/budget"
    13  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    14  )
    15  
    16  // retryOperation is the interface that holds an operation for retry.
    17  // if retryOperation returns not nil - operation will retry
    18  // if retryOperation returns nil - retry loop will break
    19  type retryOperation func(context.Context) (err error)
    20  
    21  type retryOptions struct {
    22  	label       string
    23  	call        call
    24  	trace       *trace.Retry
    25  	idempotent  bool
    26  	stackTrace  bool
    27  	fastBackoff backoff.Backoff
    28  	slowBackoff backoff.Backoff
    29  	budget      budget.Budget
    30  
    31  	panicCallback func(e interface{})
    32  }
    33  
    34  type Option interface {
    35  	ApplyRetryOption(opts *retryOptions)
    36  }
    37  
    38  var _ Option = labelOption("")
    39  
    40  type labelOption string
    41  
    42  func (label labelOption) ApplyDoOption(opts *doOptions) {
    43  	opts.retryOptions = append(opts.retryOptions, WithLabel(string(label)))
    44  }
    45  
    46  func (label labelOption) ApplyDoTxOption(opts *doTxOptions) {
    47  	opts.retryOptions = append(opts.retryOptions, WithLabel(string(label)))
    48  }
    49  
    50  func (label labelOption) ApplyRetryOption(opts *retryOptions) {
    51  	opts.label = string(label)
    52  }
    53  
    54  // WithLabel applies label for identification call Retry in trace.Retry.OnRetry
    55  func WithLabel(label string) labelOption {
    56  	return labelOption(label)
    57  }
    58  
    59  var _ Option = (*callOption)(nil)
    60  
    61  type callOption struct {
    62  	call
    63  }
    64  
    65  func (call callOption) ApplyDoOption(opts *doOptions) {
    66  	opts.retryOptions = append(opts.retryOptions, withCaller(call))
    67  }
    68  
    69  func (call callOption) ApplyDoTxOption(opts *doTxOptions) {
    70  	opts.retryOptions = append(opts.retryOptions, withCaller(call))
    71  }
    72  
    73  func (call callOption) ApplyRetryOption(opts *retryOptions) {
    74  	opts.call = call
    75  }
    76  
    77  type call interface {
    78  	FunctionID() string
    79  }
    80  
    81  func withCaller(call call) callOption {
    82  	return callOption{call}
    83  }
    84  
    85  var _ Option = stackTraceOption{}
    86  
    87  type stackTraceOption struct{}
    88  
    89  func (stackTraceOption) ApplyRetryOption(opts *retryOptions) {
    90  	opts.stackTrace = true
    91  }
    92  
    93  func (stackTraceOption) ApplyDoOption(opts *doOptions) {
    94  	opts.retryOptions = append(opts.retryOptions, WithStackTrace())
    95  }
    96  
    97  func (stackTraceOption) ApplyDoTxOption(opts *doTxOptions) {
    98  	opts.retryOptions = append(opts.retryOptions, WithStackTrace())
    99  }
   100  
   101  // WithStackTrace wraps errors with stacktrace from Retry call
   102  func WithStackTrace() stackTraceOption {
   103  	return stackTraceOption{}
   104  }
   105  
   106  var _ Option = traceOption{}
   107  
   108  type traceOption struct {
   109  	t *trace.Retry
   110  }
   111  
   112  func (t traceOption) ApplyRetryOption(opts *retryOptions) {
   113  	opts.trace = opts.trace.Compose(t.t)
   114  }
   115  
   116  func (t traceOption) ApplyDoOption(opts *doOptions) {
   117  	opts.retryOptions = append(opts.retryOptions, WithTrace(t.t))
   118  }
   119  
   120  func (t traceOption) ApplyDoTxOption(opts *doTxOptions) {
   121  	opts.retryOptions = append(opts.retryOptions, WithTrace(t.t))
   122  }
   123  
   124  // WithTrace returns trace option
   125  func WithTrace(t *trace.Retry) traceOption {
   126  	return traceOption{t: t}
   127  }
   128  
   129  var _ Option = budgetOption{}
   130  
   131  type budgetOption struct {
   132  	b budget.Budget
   133  }
   134  
   135  func (b budgetOption) ApplyRetryOption(opts *retryOptions) {
   136  	opts.budget = b.b
   137  }
   138  
   139  func (b budgetOption) ApplyDoOption(opts *doOptions) {
   140  	opts.retryOptions = append(opts.retryOptions, WithBudget(b.b))
   141  }
   142  
   143  func (b budgetOption) ApplyDoTxOption(opts *doTxOptions) {
   144  	opts.retryOptions = append(opts.retryOptions, WithBudget(b.b))
   145  }
   146  
   147  //	WithBudget returns budget option
   148  //
   149  // Experimental: https://github.com/ydb-platform/ydb-go-sdk/blob/master/VERSIONING.md#experimental
   150  func WithBudget(b budget.Budget) budgetOption {
   151  	return budgetOption{b: b}
   152  }
   153  
   154  var _ Option = idempotentOption(false)
   155  
   156  type idempotentOption bool
   157  
   158  func (idempotent idempotentOption) ApplyRetryOption(opts *retryOptions) {
   159  	opts.idempotent = bool(idempotent)
   160  }
   161  
   162  func (idempotent idempotentOption) ApplyDoOption(opts *doOptions) {
   163  	opts.retryOptions = append(opts.retryOptions, WithIdempotent(bool(idempotent)))
   164  }
   165  
   166  func (idempotent idempotentOption) ApplyDoTxOption(opts *doTxOptions) {
   167  	opts.retryOptions = append(opts.retryOptions, WithIdempotent(bool(idempotent)))
   168  }
   169  
   170  // WithIdempotent applies idempotent flag to retry operation
   171  func WithIdempotent(idempotent bool) idempotentOption {
   172  	return idempotentOption(idempotent)
   173  }
   174  
   175  var _ Option = fastBackoffOption{}
   176  
   177  type fastBackoffOption struct {
   178  	backoff backoff.Backoff
   179  }
   180  
   181  func (o fastBackoffOption) ApplyRetryOption(opts *retryOptions) {
   182  	if o.backoff != nil {
   183  		opts.fastBackoff = o.backoff
   184  	}
   185  }
   186  
   187  func (o fastBackoffOption) ApplyDoOption(opts *doOptions) {
   188  	opts.retryOptions = append(opts.retryOptions, WithFastBackoff(o.backoff))
   189  }
   190  
   191  func (o fastBackoffOption) ApplyDoTxOption(opts *doTxOptions) {
   192  	opts.retryOptions = append(opts.retryOptions, WithFastBackoff(o.backoff))
   193  }
   194  
   195  // WithFastBackoff replaces default fast backoff
   196  func WithFastBackoff(b backoff.Backoff) fastBackoffOption {
   197  	return fastBackoffOption{backoff: b}
   198  }
   199  
   200  var _ Option = slowBackoffOption{}
   201  
   202  type slowBackoffOption struct {
   203  	backoff backoff.Backoff
   204  }
   205  
   206  func (o slowBackoffOption) ApplyRetryOption(opts *retryOptions) {
   207  	if o.backoff != nil {
   208  		opts.slowBackoff = o.backoff
   209  	}
   210  }
   211  
   212  func (o slowBackoffOption) ApplyDoOption(opts *doOptions) {
   213  	opts.retryOptions = append(opts.retryOptions, WithSlowBackoff(o.backoff))
   214  }
   215  
   216  func (o slowBackoffOption) ApplyDoTxOption(opts *doTxOptions) {
   217  	opts.retryOptions = append(opts.retryOptions, WithSlowBackoff(o.backoff))
   218  }
   219  
   220  // WithSlowBackoff replaces default slow backoff
   221  func WithSlowBackoff(b backoff.Backoff) slowBackoffOption {
   222  	return slowBackoffOption{backoff: b}
   223  }
   224  
   225  var _ Option = panicCallbackOption{}
   226  
   227  type panicCallbackOption struct {
   228  	callback func(e interface{})
   229  }
   230  
   231  func (o panicCallbackOption) ApplyRetryOption(opts *retryOptions) {
   232  	opts.panicCallback = o.callback
   233  }
   234  
   235  func (o panicCallbackOption) ApplyDoOption(opts *doOptions) {
   236  	opts.retryOptions = append(opts.retryOptions, WithPanicCallback(o.callback))
   237  }
   238  
   239  func (o panicCallbackOption) ApplyDoTxOption(opts *doTxOptions) {
   240  	opts.retryOptions = append(opts.retryOptions, WithPanicCallback(o.callback))
   241  }
   242  
   243  // WithPanicCallback returns panic callback option
   244  // If not defined - panic would not intercept with driver
   245  func WithPanicCallback(panicCallback func(e interface{})) panicCallbackOption {
   246  	return panicCallbackOption{callback: panicCallback}
   247  }
   248  
   249  // Retry provide the best effort fo retrying operation
   250  //
   251  // Retry implements internal busy loop until one of the following conditions is met:
   252  //
   253  // - context was canceled or deadlined
   254  //
   255  // - retry operation returned nil as error
   256  //
   257  // Warning: if context without deadline or cancellation func was passed, Retry will work infinitely.
   258  //
   259  // # If you need to retry your op func on some logic errors - you must return RetryableError() from retryOperation
   260  func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr error) {
   261  	_, err := RetryWithResult[*struct{}](ctx, func(ctx context.Context) (*struct{}, error) {
   262  		err := op(ctx)
   263  		if err != nil {
   264  			return nil, xerrors.WithStackTrace(err)
   265  		}
   266  
   267  		return nil, nil //nolint:nilnil
   268  	}, opts...)
   269  	if err != nil {
   270  		return xerrors.WithStackTrace(err)
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  // RetryWithResult provide the best effort fo retrying operation which will return value or error
   277  //
   278  // RetryWithResult implements internal busy loop until one of the following conditions is met:
   279  //
   280  // - context was canceled or deadlined
   281  //
   282  // - retry operation returned nil as error
   283  //
   284  // Warning: if context without deadline or cancellation func was passed, RetryWithResult will work infinitely.
   285  //
   286  // # If you need to retry your op func on some logic errors - you must return RetryableError() from retryOperation
   287  //
   288  // Experimental: https://github.com/ydb-platform/ydb-go-sdk/blob/master/VERSIONING.md#experimental
   289  func RetryWithResult[T any](ctx context.Context, //nolint:revive,funlen
   290  	op func(context.Context) (T, error), opts ...Option,
   291  ) (_ T, finalErr error) {
   292  	var (
   293  		zeroValue T
   294  		options   = &retryOptions{
   295  			call:        stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/retry.RetryWithResult"),
   296  			trace:       &trace.Retry{},
   297  			budget:      budget.Limited(-1),
   298  			fastBackoff: backoff.Fast,
   299  			slowBackoff: backoff.Slow,
   300  		}
   301  	)
   302  	for _, opt := range opts {
   303  		if opt != nil {
   304  			opt.ApplyRetryOption(options)
   305  		}
   306  	}
   307  	if options.idempotent {
   308  		ctx = xcontext.WithIdempotent(ctx, options.idempotent)
   309  	}
   310  
   311  	defer func() {
   312  		if finalErr != nil && options.stackTrace {
   313  			//nolint:gomnd
   314  			finalErr = xerrors.WithStackTrace(finalErr,
   315  				xerrors.WithSkipDepth(2), // 1 - exit from defer, 1 - exit from Retry call
   316  			)
   317  		}
   318  	}()
   319  	var (
   320  		i        int
   321  		attempts int
   322  		lastErr  error
   323  
   324  		code   = int64(0)
   325  		onDone = trace.RetryOnRetry(options.trace, &ctx,
   326  			options.call, options.label, options.idempotent, xcontext.IsNestedCall(ctx),
   327  		)
   328  	)
   329  	defer func() {
   330  		onDone(attempts, finalErr)
   331  	}()
   332  	for {
   333  		i++
   334  		attempts++
   335  		select {
   336  		case <-ctx.Done():
   337  			return zeroValue, xerrors.WithStackTrace(xerrors.Join(
   338  				fmt.Errorf("retry failed on attempt No.%d: %w", attempts, ctx.Err()),
   339  				lastErr,
   340  			))
   341  
   342  		default:
   343  			v, err := opWithRecover(ctx, options, op)
   344  
   345  			if err == nil {
   346  				return v, nil
   347  			}
   348  
   349  			m := Check(err)
   350  
   351  			if m.StatusCode() != code {
   352  				i = 0
   353  			}
   354  
   355  			code = m.StatusCode()
   356  
   357  			if !m.MustRetry(options.idempotent) {
   358  				return zeroValue, xerrors.WithStackTrace(xerrors.Join(
   359  					fmt.Errorf("non-retryable error occurred on attempt No.%d (idempotent=%v): %w",
   360  						attempts, options.idempotent, err),
   361  					lastErr,
   362  				))
   363  			}
   364  
   365  			t := time.NewTimer(backoff.Delay(m.BackoffType(), i,
   366  				backoff.WithFastBackoff(options.fastBackoff),
   367  				backoff.WithSlowBackoff(options.slowBackoff),
   368  			))
   369  
   370  			select {
   371  			case <-ctx.Done():
   372  				t.Stop()
   373  
   374  				return zeroValue, xerrors.WithStackTrace(
   375  					xerrors.Join(
   376  						fmt.Errorf("attempt No.%d: %w", attempts, ctx.Err()),
   377  						err,
   378  						lastErr,
   379  					),
   380  				)
   381  			case <-t.C:
   382  				t.Stop()
   383  
   384  				if acquireErr := options.budget.Acquire(ctx); acquireErr != nil {
   385  					return zeroValue, xerrors.WithStackTrace(
   386  						xerrors.Join(
   387  							fmt.Errorf("attempt No.%d: %w", attempts, budget.ErrNoQuota),
   388  							acquireErr,
   389  							err,
   390  							lastErr,
   391  						),
   392  					)
   393  				}
   394  			}
   395  
   396  			lastErr = err
   397  		}
   398  	}
   399  }
   400  
   401  func opWithRecover[T any](ctx context.Context,
   402  	options *retryOptions, op func(context.Context) (T, error),
   403  ) (_ T, finalErr error) {
   404  	var zeroValue T
   405  	if options.panicCallback != nil {
   406  		defer func() {
   407  			if e := recover(); e != nil {
   408  				options.panicCallback(e)
   409  				finalErr = xerrors.WithStackTrace(
   410  					fmt.Errorf("panic recovered: %v", e),
   411  				)
   412  			}
   413  		}()
   414  	}
   415  
   416  	v, err := op(ctx)
   417  	if err != nil {
   418  		return zeroValue, xerrors.WithStackTrace(err)
   419  	}
   420  
   421  	return v, nil
   422  }
   423  
   424  // Check returns retry mode for queryErr.
   425  func Check(err error) (m retryMode) {
   426  	code, errType, backoffType, invalidObject := xerrors.Check(err)
   427  
   428  	return retryMode{
   429  		code:               code,
   430  		errType:            errType,
   431  		backoff:            backoffType,
   432  		isRetryObjectValid: !invalidObject,
   433  	}
   434  }