Repository navigation
Retry and Timeout
Package: Carom. Classes: Carom, Bounce, Bounce<T>.
using Carom;
// Up to 3 retries, so at most 4 calls
var user = await Carom.ShotAsync(ct => api.GetUserAsync(id, ct), retries: 3);
// Same thing with a Bounce
var bounce = Bounce.Times(5)
.WithDelay(TimeSpan.FromMilliseconds(200))
.WithMaxDelay(TimeSpan.FromSeconds(5))
.WithTimeout(TimeSpan.FromSeconds(30))
.When(ex => ex is HttpRequestException);
var data = await Carom.ShotAsync(ct => api.FetchAsync(ct), bounce, cancellationToken);retries is the number of retries, not attempts. Bounce.Times(3) makes up to 4 calls. A negative count is treated as 0.
The delay between attempts uses decorrelated jitter: the next delay is a random value between the base delay and three times the previous delay, capped by MaxDelay.
| Setting | Default | Notes |
|---|---|---|
WithDelay |
100 ms | Negative throws ArgumentOutOfRangeException
|
WithMaxDelay |
30 s | Must be positive. A base delay above the cap is clamped to it |
WithoutJitter |
off | Fixed backoff of base times 2 to the power of the attempt, capped. Every client retries in step, so leave jitter on unless you have a reason |
Without When, every exception retries except OperationCanceledException. With When, only exceptions the predicate accepts retry. Cancellation never retries, whatever the predicate says. Bounce.On<TException>() is shorthand for When(ex => ex is TException).
var bounce = Bounce.For<HttpResponseMessage>(retries: 3)
.WhenResult(r => (int)r.StatusCode >= 500);
var response = await Carom.ShotAsync(ct => http.GetAsync(url, ct), bounce);When retries run out, the last result is returned, not an exception.
WithTimeout covers the whole operation, every attempt and every backoff delay, not each attempt. When it fires you get TimeoutRejectedException, which has a Timeout property. When your own token is cancelled you get OperationCanceledException instead, so the two can be told apart.
Use the overloads that take Func<CancellationToken, Task>. They pass the linked token to your action, so the timeout cancels the work itself. With the overloads that take Func<Task>, Carom stops waiting when the timeout fires but cannot stop the operation, which keeps running in the background.
The timeout is async only. Passing a Bounce with a timeout to a synchronous Shot overload throws InvalidOperationException, because the sync path has no way to enforce it.
Bounce is a readonly struct, and each fluent call returns a new copy. The Carom hot path allocates nothing beyond what your delegate captures. See Benchmarks.
Written from the Carom source. The repository is the source of truth; if a page disagrees with the code, the code wins. Licensed MPL-2.0.
Core
Carom.Extensions
Integrations
Project