| 449 | } |
| 450 | |
| 451 | func RetryLoop[T any](ctx context.Context, description string, worker RetryWorker[T], sleeper RetrySleeper) (error, T) { |
| 452 | |
| 453 | numAttempts := 1 |
| 454 | |
| 455 | for { |
| 456 | shouldRetry, err, value := worker() |
| 457 | if !shouldRetry { |
| 458 | if err != nil { |
| 459 | return err, *new(T) |
| 460 | } |
| 461 | return nil, value |
| 462 | } |
| 463 | shouldContinue, sleepMs := sleeper(numAttempts) |
| 464 | if !shouldContinue { |
| 465 | if err == nil { |
| 466 | err = NewRetryTimeoutError(description, numAttempts) |
| 467 | } |
| 468 | WarnfCtx(ctx, "RetryLoop for %v giving up after %v attempts", description, numAttempts) |
| 469 | return err, value |
| 470 | } |
| 471 | DebugfCtx(ctx, KeyAll, "RetryLoop retrying %v after %v ms.", description, sleepMs) |
| 472 | |
| 473 | select { |
| 474 | case <-ctx.Done(): |
| 475 | verb := "closed" |
| 476 | ctxErr := ctx.Err() |
| 477 | if errors.Is(ctxErr, context.Canceled) { |
| 478 | verb = "canceled" |
| 479 | } else if errors.Is(ctxErr, context.DeadlineExceeded) { |
| 480 | verb = "timed out" |
| 481 | } |
| 482 | return fmt.Errorf("Retry loop for %v %s based on context: %w", description, verb, context.Cause(ctx)), *new(T) |
| 483 | case <-time.After(time.Millisecond * time.Duration(sleepMs)): |
| 484 | } |
| 485 | |
| 486 | numAttempts += 1 |
| 487 | |
| 488 | } |
| 489 | } |
| 490 | |
| 491 | // A version of RetryLoop that returns a strongly typed cas as uint64, to avoid interface conversion overhead for |
| 492 | // high throughput operations. |