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 }