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

     1  package retry
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"fmt"
     7  
     8  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/stack"
     9  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xcontext"
    10  	"github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors"
    11  	budget "github.com/ydb-platform/ydb-go-sdk/v3/retry/budget"
    12  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    13  )
    14  
    15  type doOptions struct {
    16  	retryOptions []Option
    17  }
    18  
    19  // doTxOption defines option for redefine default Retry behavior
    20  type doOption interface {
    21  	ApplyDoOption(opts *doOptions)
    22  }
    23  
    24  var (
    25  	_ doOption = doRetryOptionsOption(nil)
    26  	_ doOption = labelOption("")
    27  )
    28  
    29  type doRetryOptionsOption []Option
    30  
    31  func (retryOptions doRetryOptionsOption) ApplyDoOption(opts *doOptions) {
    32  	opts.retryOptions = append(opts.retryOptions, retryOptions...)
    33  }
    34  
    35  // WithDoRetryOptions specified retry options
    36  // Deprecated: use explicit options instead.
    37  // Will be removed after Oct 2024.
    38  // Read about versioning policy: https://github.com/ydb-platform/ydb-go-sdk/blob/master/VERSIONING.md#deprecated
    39  func WithDoRetryOptions(opts ...Option) doRetryOptionsOption {
    40  	return opts
    41  }
    42  
    43  // Do is a retryer of database/sql Conn with fallbacks on errors
    44  func Do(ctx context.Context, db *sql.DB, op func(ctx context.Context, cc *sql.Conn) error, opts ...doOption) error {
    45  	_, err := DoWithResult(ctx, db, func(ctx context.Context, cc *sql.Conn) (*struct{}, error) {
    46  		err := op(ctx, cc)
    47  		if err != nil {
    48  			return nil, xerrors.WithStackTrace(err)
    49  		}
    50  
    51  		return nil, nil //nolint:nilnil
    52  	}, opts...)
    53  	if err != nil {
    54  		return xerrors.WithStackTrace(err)
    55  	}
    56  
    57  	return nil
    58  }
    59  
    60  // DoWithResult is a retryer of database/sql Conn with fallbacks on errors
    61  //
    62  // Experimental: https://github.com/ydb-platform/ydb-go-sdk/blob/master/VERSIONING.md#experimental
    63  func DoWithResult[T any](ctx context.Context, db *sql.DB,
    64  	op func(ctx context.Context, cc *sql.Conn) (T, error),
    65  	opts ...doOption,
    66  ) (T, error) {
    67  	var (
    68  		zeroValue T
    69  		options   = doOptions{
    70  			retryOptions: []Option{
    71  				withCaller(stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/retry.DoWithResult")),
    72  			},
    73  		}
    74  		attempts = 0
    75  	)
    76  	if tracer, has := db.Driver().(interface {
    77  		TraceRetry() *trace.Retry
    78  	}); has {
    79  		options.retryOptions = append(options.retryOptions, nil)
    80  		copy(options.retryOptions[1:], options.retryOptions)
    81  		options.retryOptions[0] = WithTrace(tracer.TraceRetry())
    82  	}
    83  	for _, opt := range opts {
    84  		if opt != nil {
    85  			opt.ApplyDoOption(&options)
    86  		}
    87  	}
    88  	v, err := RetryWithResult(ctx, func(ctx context.Context) (T, error) {
    89  		attempts++
    90  		cc, err := db.Conn(ctx)
    91  		if err != nil {
    92  			return zeroValue, unwrapErrBadConn(xerrors.WithStackTrace(err))
    93  		}
    94  		defer func() {
    95  			_ = cc.Close()
    96  		}()
    97  		v, err := op(xcontext.MarkRetryCall(ctx), cc)
    98  		if err != nil {
    99  			return zeroValue, unwrapErrBadConn(xerrors.WithStackTrace(err))
   100  		}
   101  
   102  		return v, nil
   103  	}, options.retryOptions...)
   104  	if err != nil {
   105  		return zeroValue, xerrors.WithStackTrace(
   106  			fmt.Errorf("operation failed with %d attempts: %w", attempts, err),
   107  		)
   108  	}
   109  
   110  	return v, nil
   111  }
   112  
   113  type doTxOptions struct {
   114  	txOptions    *sql.TxOptions
   115  	retryOptions []Option
   116  }
   117  
   118  // doTxOption defines option for redefine default Retry behavior
   119  type doTxOption interface {
   120  	ApplyDoTxOption(o *doTxOptions)
   121  }
   122  
   123  var _ doTxOption = doTxRetryOptionsOption(nil)
   124  
   125  type doTxRetryOptionsOption []Option
   126  
   127  func (doTxRetryOptions doTxRetryOptionsOption) ApplyDoTxOption(o *doTxOptions) {
   128  	o.retryOptions = append(o.retryOptions, doTxRetryOptions...)
   129  }
   130  
   131  // WithDoTxRetryOptions specified retry options
   132  // Deprecated: use explicit options instead.
   133  // Will be removed after Oct 2024.
   134  // Read about versioning policy: https://github.com/ydb-platform/ydb-go-sdk/blob/master/VERSIONING.md#deprecated
   135  func WithDoTxRetryOptions(opts ...Option) doTxRetryOptionsOption {
   136  	return opts
   137  }
   138  
   139  var _ doTxOption = txOptionsOption{}
   140  
   141  type txOptionsOption struct {
   142  	txOptions *sql.TxOptions
   143  }
   144  
   145  func (txOptions txOptionsOption) ApplyDoTxOption(o *doTxOptions) {
   146  	o.txOptions = txOptions.txOptions
   147  }
   148  
   149  // WithTxOptions specified transaction options
   150  func WithTxOptions(txOptions *sql.TxOptions) txOptionsOption {
   151  	return txOptionsOption{
   152  		txOptions: txOptions,
   153  	}
   154  }
   155  
   156  // DoTx is a retryer of database/sql transactions with fallbacks on errors
   157  func DoTx(ctx context.Context, db *sql.DB, op func(context.Context, *sql.Tx) error, opts ...doTxOption) error {
   158  	_, err := DoTxWithResult(ctx, db, func(ctx context.Context, tx *sql.Tx) (*struct{}, error) {
   159  		err := op(ctx, tx)
   160  		if err != nil {
   161  			return nil, xerrors.WithStackTrace(err)
   162  		}
   163  
   164  		return nil, nil //nolint:nilnil
   165  	}, opts...)
   166  	if err != nil {
   167  		return xerrors.WithStackTrace(err)
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // DoTxWithResult is a retryer of database/sql transactions with fallbacks on errors
   174  //
   175  // Experimental: https://github.com/ydb-platform/ydb-go-sdk/blob/master/VERSIONING.md#experimental
   176  func DoTxWithResult[T any](ctx context.Context, db *sql.DB, //nolint:funlen
   177  	op func(context.Context, *sql.Tx) (T, error),
   178  	opts ...doTxOption,
   179  ) (T, error) {
   180  	var (
   181  		zeroValue T
   182  		options   = doTxOptions{
   183  			retryOptions: []Option{
   184  				withCaller(stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/v3/retry.DoTxWithResult")),
   185  			},
   186  			txOptions: &sql.TxOptions{
   187  				Isolation: sql.LevelDefault,
   188  				ReadOnly:  false,
   189  			},
   190  		}
   191  		attempts = 0
   192  	)
   193  	if d, has := db.Driver().(interface {
   194  		TraceRetry() *trace.Retry
   195  		RetryBudget() budget.Budget
   196  	}); has {
   197  		options.retryOptions = append(options.retryOptions, nil, nil)
   198  		copy(options.retryOptions[2:], options.retryOptions)
   199  		options.retryOptions[0] = WithTrace(d.TraceRetry())
   200  		options.retryOptions[1] = WithBudget(d.RetryBudget())
   201  	}
   202  	for _, opt := range opts {
   203  		if opt != nil {
   204  			opt.ApplyDoTxOption(&options)
   205  		}
   206  	}
   207  	v, err := RetryWithResult(ctx, func(ctx context.Context) (_ T, finalErr error) {
   208  		attempts++
   209  		tx, err := db.BeginTx(ctx, options.txOptions)
   210  		if err != nil {
   211  			return zeroValue, unwrapErrBadConn(xerrors.WithStackTrace(err))
   212  		}
   213  		defer func() {
   214  			if finalErr == nil {
   215  				return
   216  			}
   217  			errRollback := tx.Rollback()
   218  			if errRollback == nil {
   219  				return
   220  			}
   221  			finalErr = xerrors.NewWithIssues("",
   222  				xerrors.WithStackTrace(finalErr),
   223  				xerrors.WithStackTrace(fmt.Errorf("rollback failed: %w", errRollback)),
   224  			)
   225  		}()
   226  		v, err := op(xcontext.MarkRetryCall(ctx), tx)
   227  		if err != nil {
   228  			return zeroValue, unwrapErrBadConn(xerrors.WithStackTrace(err))
   229  		}
   230  		if err = tx.Commit(); err != nil {
   231  			return zeroValue, unwrapErrBadConn(xerrors.WithStackTrace(err))
   232  		}
   233  
   234  		return v, nil
   235  	}, options.retryOptions...)
   236  	if err != nil {
   237  		return zeroValue, xerrors.WithStackTrace(
   238  			fmt.Errorf("tx operation failed with %d attempts: %w", attempts, err),
   239  		)
   240  	}
   241  
   242  	return v, nil
   243  }