github.com/ydb-platform/ydb-go-sdk/v3@v3.57.0/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  	"github.com/ydb-platform/ydb-go-sdk/v3/trace"
    12  )
    13  
    14  type doOptions struct {
    15  	retryOptions []Option
    16  }
    17  
    18  // doTxOption defines option for redefine default Retry behavior
    19  type doOption interface {
    20  	ApplyDoOption(opts *doOptions)
    21  }
    22  
    23  var (
    24  	_ doOption = doRetryOptionsOption(nil)
    25  	_ doOption = labelOption("")
    26  )
    27  
    28  type doRetryOptionsOption []Option
    29  
    30  func (retryOptions doRetryOptionsOption) ApplyDoOption(opts *doOptions) {
    31  	opts.retryOptions = append(opts.retryOptions, retryOptions...)
    32  }
    33  
    34  // WithDoRetryOptions specified retry options
    35  // Deprecated: use implicit options instead
    36  func WithDoRetryOptions(opts ...Option) doRetryOptionsOption {
    37  	return opts
    38  }
    39  
    40  // Do is a retryer of database/sql Conn with fallbacks on errors
    41  func Do(ctx context.Context, db *sql.DB, op func(ctx context.Context, cc *sql.Conn) error, opts ...doOption) error {
    42  	var (
    43  		options = doOptions{
    44  			retryOptions: []Option{
    45  				withCaller(stack.FunctionID("")),
    46  			},
    47  		}
    48  		attempts = 0
    49  	)
    50  	if tracer, has := db.Driver().(interface {
    51  		TraceRetry() *trace.Retry
    52  	}); has {
    53  		options.retryOptions = append(options.retryOptions, nil)
    54  		copy(options.retryOptions[1:], options.retryOptions)
    55  		options.retryOptions[0] = WithTrace(tracer.TraceRetry())
    56  	}
    57  	for _, opt := range opts {
    58  		if opt != nil {
    59  			opt.ApplyDoOption(&options)
    60  		}
    61  	}
    62  	err := Retry(ctx, func(ctx context.Context) error {
    63  		attempts++
    64  		cc, err := db.Conn(ctx)
    65  		if err != nil {
    66  			return unwrapErrBadConn(xerrors.WithStackTrace(err))
    67  		}
    68  		defer func() {
    69  			_ = cc.Close()
    70  		}()
    71  		if err = op(xcontext.MarkRetryCall(ctx), cc); err != nil {
    72  			return unwrapErrBadConn(xerrors.WithStackTrace(err))
    73  		}
    74  
    75  		return nil
    76  	}, options.retryOptions...)
    77  	if err != nil {
    78  		return xerrors.WithStackTrace(
    79  			fmt.Errorf("operation failed with %d attempts: %w", attempts, err),
    80  		)
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  type doTxOptions struct {
    87  	txOptions    *sql.TxOptions
    88  	retryOptions []Option
    89  }
    90  
    91  // doTxOption defines option for redefine default Retry behavior
    92  type doTxOption interface {
    93  	ApplyDoTxOption(o *doTxOptions)
    94  }
    95  
    96  var _ doTxOption = doTxRetryOptionsOption(nil)
    97  
    98  type doTxRetryOptionsOption []Option
    99  
   100  func (doTxRetryOptions doTxRetryOptionsOption) ApplyDoTxOption(o *doTxOptions) {
   101  	o.retryOptions = append(o.retryOptions, doTxRetryOptions...)
   102  }
   103  
   104  // WithDoTxRetryOptions specified retry options
   105  // Deprecated: use implicit options instead
   106  func WithDoTxRetryOptions(opts ...Option) doTxRetryOptionsOption {
   107  	return opts
   108  }
   109  
   110  var _ doTxOption = txOptionsOption{}
   111  
   112  type txOptionsOption struct {
   113  	txOptions *sql.TxOptions
   114  }
   115  
   116  func (txOptions txOptionsOption) ApplyDoTxOption(o *doTxOptions) {
   117  	o.txOptions = txOptions.txOptions
   118  }
   119  
   120  // WithTxOptions specified transaction options
   121  func WithTxOptions(txOptions *sql.TxOptions) txOptionsOption {
   122  	return txOptionsOption{
   123  		txOptions: txOptions,
   124  	}
   125  }
   126  
   127  // DoTx is a retryer of database/sql transactions with fallbacks on errors
   128  func DoTx(ctx context.Context, db *sql.DB, op func(context.Context, *sql.Tx) error, opts ...doTxOption) error {
   129  	var (
   130  		options = doTxOptions{
   131  			retryOptions: []Option{
   132  				withCaller(stack.FunctionID("")),
   133  			},
   134  			txOptions: &sql.TxOptions{
   135  				Isolation: sql.LevelDefault,
   136  				ReadOnly:  false,
   137  			},
   138  		}
   139  		attempts = 0
   140  	)
   141  	if tracer, has := db.Driver().(interface {
   142  		TraceRetry() *trace.Retry
   143  	}); has {
   144  		options.retryOptions = append(options.retryOptions, nil)
   145  		copy(options.retryOptions[1:], options.retryOptions)
   146  		options.retryOptions[0] = WithTrace(tracer.TraceRetry())
   147  	}
   148  	for _, opt := range opts {
   149  		if opt != nil {
   150  			opt.ApplyDoTxOption(&options)
   151  		}
   152  	}
   153  	err := Retry(ctx, func(ctx context.Context) (finalErr error) {
   154  		attempts++
   155  		tx, err := db.BeginTx(ctx, options.txOptions)
   156  		if err != nil {
   157  			return unwrapErrBadConn(xerrors.WithStackTrace(err))
   158  		}
   159  		defer func() {
   160  			if finalErr == nil {
   161  				return
   162  			}
   163  			errRollback := tx.Rollback()
   164  			if errRollback == nil {
   165  				return
   166  			}
   167  			finalErr = xerrors.NewWithIssues("",
   168  				xerrors.WithStackTrace(finalErr),
   169  				xerrors.WithStackTrace(fmt.Errorf("rollback failed: %w", errRollback)),
   170  			)
   171  		}()
   172  		if err = op(xcontext.MarkRetryCall(ctx), tx); err != nil {
   173  			return unwrapErrBadConn(xerrors.WithStackTrace(err))
   174  		}
   175  		if err = tx.Commit(); err != nil {
   176  			return unwrapErrBadConn(xerrors.WithStackTrace(err))
   177  		}
   178  
   179  		return nil
   180  	}, options.retryOptions...)
   181  	if err != nil {
   182  		return xerrors.WithStackTrace(
   183  			fmt.Errorf("tx operation failed with %d attempts: %w", attempts, err),
   184  		)
   185  	}
   186  
   187  	return nil
   188  }