| | | 1 | | namespace AsiBackbone.Signing.ManagedKey; |
| | | 2 | | |
| | | 3 | | /// <summary> |
| | | 4 | | /// Calculates bounded exponential retry delays with jitter. |
| | | 5 | | /// </summary> |
| | | 6 | | internal static class ManagedKeyRetryBackoff |
| | | 7 | | { |
| | | 8 | | internal const string StrategyName = "exponential-jitter"; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Calculates a monotonically increasing retry delay for the one-based retry attempt. |
| | | 12 | | /// </summary> |
| | | 13 | | internal static TimeSpan CalculateDelay( |
| | | 14 | | TimeSpan baseDelay, |
| | | 15 | | TimeSpan maxDelay, |
| | | 16 | | int retryAttempt, |
| | | 17 | | double jitterSample, |
| | | 18 | | TimeSpan previousDelay) |
| | | 19 | | { |
| | | 20 | | ArgumentOutOfRangeException.ThrowIfLessThan(baseDelay, TimeSpan.Zero); |
| | | 21 | | |
| | | 22 | | if (maxDelay < TimeSpan.Zero || maxDelay < baseDelay) |
| | | 23 | | { |
| | | 24 | | throw new ArgumentOutOfRangeException(nameof(maxDelay)); |
| | | 25 | | } |
| | | 26 | | |
| | | 27 | | ArgumentOutOfRangeException.ThrowIfNegativeOrZero(retryAttempt); |
| | | 28 | | |
| | | 29 | | if (double.IsNaN(jitterSample) || jitterSample < 0d || jitterSample > 1d) |
| | | 30 | | { |
| | | 31 | | throw new ArgumentOutOfRangeException(nameof(jitterSample)); |
| | | 32 | | } |
| | | 33 | | |
| | | 34 | | if (previousDelay < TimeSpan.Zero || previousDelay > maxDelay) |
| | | 35 | | { |
| | | 36 | | throw new ArgumentOutOfRangeException(nameof(previousDelay)); |
| | | 37 | | } |
| | | 38 | | |
| | | 39 | | if (baseDelay == TimeSpan.Zero) |
| | | 40 | | { |
| | | 41 | | return TimeSpan.Zero; |
| | | 42 | | } |
| | | 43 | | |
| | | 44 | | long upperBoundTicks = CalculateUpperBoundTicks(baseDelay.Ticks, maxDelay.Ticks, retryAttempt); |
| | | 45 | | long exponentialLowerBoundTicks = upperBoundTicks / 2; |
| | | 46 | | long monotonicLowerBoundTicks = previousDelay.Ticks < upperBoundTicks |
| | | 47 | | ? previousDelay.Ticks + 1 |
| | | 48 | | : upperBoundTicks; |
| | | 49 | | long lowerBoundTicks = Math.Min( |
| | | 50 | | upperBoundTicks, |
| | | 51 | | Math.Max(exponentialLowerBoundTicks, monotonicLowerBoundTicks)); |
| | | 52 | | long jitterRangeTicks = upperBoundTicks - lowerBoundTicks; |
| | | 53 | | long jitteredTicks = jitterSample >= 1d |
| | | 54 | | ? upperBoundTicks |
| | | 55 | | : lowerBoundTicks + (long)(jitterRangeTicks * jitterSample); |
| | | 56 | | |
| | | 57 | | return TimeSpan.FromTicks(Math.Min(jitteredTicks, maxDelay.Ticks)); |
| | | 58 | | } |
| | | 59 | | |
| | | 60 | | private static long CalculateUpperBoundTicks(long baseDelayTicks, long maxDelayTicks, int retryAttempt) |
| | | 61 | | { |
| | | 62 | | long upperBoundTicks = Math.Min(baseDelayTicks, maxDelayTicks); |
| | | 63 | | |
| | | 64 | | for (int attempt = 1; attempt < retryAttempt && upperBoundTicks < maxDelayTicks; attempt++) |
| | | 65 | | { |
| | | 66 | | upperBoundTicks = upperBoundTicks > maxDelayTicks / 2 |
| | | 67 | | ? maxDelayTicks |
| | | 68 | | : Math.Min(upperBoundTicks * 2, maxDelayTicks); |
| | | 69 | | } |
| | | 70 | | |
| | | 71 | | return upperBoundTicks; |
| | | 72 | | } |
| | | 73 | | } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Supplies a random sample used by the retry backoff calculation. |
| | | 77 | | /// </summary> |
| | | 78 | | internal interface IManagedKeyRetryJitterSource |
| | | 79 | | { |
| | | 80 | | double NextDouble(); |
| | | 81 | | } |
| | | 82 | | |
| | | 83 | | /// <summary> |
| | | 84 | | /// Delays a retry while preserving cancellation. |
| | | 85 | | /// </summary> |
| | | 86 | | internal interface IManagedKeyRetryDelay |
| | | 87 | | { |
| | | 88 | | ValueTask DelayAsync(TimeSpan delay, CancellationToken cancellationToken); |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | internal sealed class SharedRandomManagedKeyRetryJitterSource : IManagedKeyRetryJitterSource |
| | | 92 | | { |
| | | 93 | | internal static SharedRandomManagedKeyRetryJitterSource Instance { get; } = new(); |
| | | 94 | | |
| | | 95 | | private SharedRandomManagedKeyRetryJitterSource() |
| | | 96 | | { |
| | | 97 | | } |
| | | 98 | | |
| | | 99 | | public double NextDouble() |
| | | 100 | | { |
| | | 101 | | return Random.Shared.NextDouble(); |
| | | 102 | | } |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | internal sealed class SystemManagedKeyRetryDelay : IManagedKeyRetryDelay |
| | | 106 | | { |
| | 70 | 107 | | internal static SystemManagedKeyRetryDelay Instance { get; } = new(); |
| | | 108 | | |
| | 2 | 109 | | private SystemManagedKeyRetryDelay() |
| | | 110 | | { |
| | 2 | 111 | | } |
| | | 112 | | |
| | | 113 | | public ValueTask DelayAsync(TimeSpan delay, CancellationToken cancellationToken) |
| | | 114 | | { |
| | 6 | 115 | | return delay <= TimeSpan.Zero |
| | 6 | 116 | | ? ValueTask.CompletedTask |
| | 6 | 117 | | : new ValueTask(Task.Delay(delay, cancellationToken)); |
| | | 118 | | } |
| | | 119 | | } |