< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Callback.DefaultCallbackRetryPolicy
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Callback/DefaultCallbackRetryPolicy.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
97%
Covered lines: 33
Uncovered lines: 1
Coverable lines: 34
Total lines: 122
Line coverage: 97%
Branch coverage
94%
Covered branches: 36
Total branches: 38
Branch coverage: 94.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 01/02/2026 - 00:16:25 Line coverage: 97% (33/34) Branch coverage: 94.7% (36/38) Total lines: 119 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/08/2026 - 08:19:25 Line coverage: 97% (33/34) Branch coverage: 94.7% (36/38) Total lines: 122 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d 01/02/2026 - 00:16:25 Line coverage: 97% (33/34) Branch coverage: 94.7% (36/38) Total lines: 119 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d001/08/2026 - 08:19:25 Line coverage: 97% (33/34) Branch coverage: 94.7% (36/38) Total lines: 122 Tag: Kestrun/Kestrun@6ab94ca7560634c2ac58b36c2b98e2a9b1bf305d

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
Evaluate(...)100%66100%
Stop(...)100%11100%
IsPermanentFailure(...)100%1212100%
IsRetryable(...)93.75%171683.33%
ComputeBackoff(...)50%22100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Callback/DefaultCallbackRetryPolicy.cs

#LineLine coverage
 1namespace Kestrun.Callback;
 2/// <summary>
 3/// Default implementation of <see cref="ICallbackRetryPolicy"/> using exponential backoff with jitter.
 4/// </summary>
 5/// <remarks>
 6/// Initializes a new instance of the <see cref="DefaultCallbackRetryPolicy"/> class.
 7/// </remarks>
 8public sealed class DefaultCallbackRetryPolicy : ICallbackRetryPolicy
 9{
 10    private readonly int _maxAttempts;
 11    private readonly TimeSpan _baseDelay;
 12    private readonly TimeSpan _maxDelay;
 1513    private readonly Random _rng = new();
 14
 15    /// <summary>
 16    /// Initializes a new instance of the <see cref="DefaultCallbackRetryPolicy"/> class with the specified options.
 17    /// </summary>
 18    /// <param name="options"> The options to configure the retry policy. </param>
 1519    public DefaultCallbackRetryPolicy(CallbackDispatchOptions? options)
 20    {
 1521        if (options == null)
 22        {
 123            _maxAttempts = 3;
 124            _baseDelay = TimeSpan.FromSeconds(2);
 125            _maxDelay = TimeSpan.FromSeconds(30);
 126            return;
 27        }
 1428        _maxAttempts = options.MaxAttempts;
 1429        _baseDelay = options.BaseDelay;
 1430        _maxDelay = options.MaxDelay;
 1431    }
 32
 33    /// <summary>
 34    /// Evaluates the given callback request and result to determine the retry decision.
 35    /// </summary>
 36    /// <param name="req"> The callback request to evaluate. </param>
 37    /// <param name="result"> The result of the callback request. </param>
 38    /// <returns> A decision indicating whether to retry or stop. </returns>
 39    public RetryDecision Evaluate(CallbackRequest req, CallbackResult result)
 40    {
 1541        var nextAttemptNumber = req.Attempt + 1;
 42
 1543        if (nextAttemptNumber >= _maxAttempts)
 44        {
 145            return Stop(req, "MaxAttemptsReached");
 46        }
 47
 1448        if (IsPermanentFailure(result))
 49        {
 650            return Stop(req, "PermanentFailure");
 51        }
 52
 853        if (!IsRetryable(result))
 54        {
 355            return Stop(req, "NotRetryable");
 56        }
 57
 558        var delay = ComputeBackoff(nextAttemptNumber);
 559        var next = DateTimeOffset.UtcNow.Add(delay);
 60
 61        // Respect Retry-After if you capture it (nice-to-have)
 62        // if (result.RetryAfter != null) { ... }
 63
 564        return new RetryDecision(RetryDecisionKind.Retry, next, delay, "RetryableFailure");
 65    }
 66    /// <summary>
 67    /// Creates a Stop retry decision with the given reason.
 68    /// </summary>
 69    /// <param name="req"> The callback request for which to create the stop decision. </param>
 70    /// <param name="reason"> The reason for stopping retries. </param>
 71    /// <returns>A RetryDecision indicating to stop retries.</returns>
 72    private static RetryDecision Stop(CallbackRequest req, string reason)
 1073        => new(RetryDecisionKind.Stop, req.NextAttemptAt, TimeSpan.Zero, reason);
 74
 75    /// <summary>
 76    /// Determines if the callback result indicates a permanent failure.
 77    /// </summary>
 78    /// <param name="r">The callback result to evaluate.</param>
 79    /// <returns>True if the result indicates a permanent failure; otherwise, false.</returns>
 80    /// <remarks>
 81    /// Permanent failures are those that should not be retried.
 82    /// could treat repeated 401/403 as permanent immediately
 83    /// </remarks>
 84    private static bool IsPermanentFailure(CallbackResult r) =>
 1485        r.StatusCode is 400 or 401 or 403 or 404 or 409 or 422;
 86
 87    /// <summary>
 88    /// Determines if the callback result indicates a retryable failure.
 89    /// </summary>
 90    /// <param name="r">The callback result to evaluate.</param>
 91    /// <returns>True if the result indicates a retryable failure; otherwise, false.</returns>
 92    private static bool IsRetryable(CallbackResult r)
 93    {
 894        if (r.ErrorType is "Timeout" or "HttpRequestException")
 95        {
 196            return true;
 97        }
 98
 799        if (r.StatusCode is null)
 100        {
 0101            return false;
 102        }
 103
 7104        var sc = r.StatusCode.Value;
 7105        return sc is 408 or 429 or (>= 500 and <= 599);
 106    }
 107    /// <summary>
 108    /// Computes the exponential backoff delay with jitter based on the attempt number.
 109    /// </summary>
 110    /// <param name="attemptNumber">The current attempt number (1-based).</param>
 111    /// <returns>A TimeSpan representing the delay before the next retry.</returns>
 112    private TimeSpan ComputeBackoff(int attemptNumber)
 113    {
 114        // attemptNumber: 1..N
 5115        var exp = Math.Pow(2, attemptNumber - 1);
 5116        var raw = TimeSpan.FromMilliseconds(_baseDelay.TotalMilliseconds * exp);
 117
 5118        var capped = raw <= _maxDelay ? raw : _maxDelay;
 5119        var jitter = TimeSpan.FromMilliseconds(_rng.Next(0, 250));
 5120        return capped + jitter;
 121    }
 122}