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 }