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 }