| | 1 | | // Copyright (c) Microsoft Corporation. All rights reserved. |
| | 2 | | // Licensed under the MIT License. |
| | 3 | |
|
| | 4 | | using System; |
| | 5 | | using System.Globalization; |
| | 6 | | using System.Net.Sockets; |
| | 7 | | using System.Threading; |
| | 8 | | using System.Threading.Tasks; |
| | 9 | | using System.Transactions; |
| | 10 | | using Azure.Core; |
| | 11 | |
|
| | 12 | | namespace Azure.Messaging.ServiceBus.Core |
| | 13 | | { |
| | 14 | | /// <summary> |
| | 15 | | /// The default retry policy for the Service Bus client library, respecting the |
| | 16 | | /// configuration specified as a set of <see cref="ServiceBusRetryOptions" />. |
| | 17 | | /// </summary> |
| | 18 | | /// |
| | 19 | | /// <seealso cref="ServiceBusRetryOptions"/> |
| | 20 | | /// |
| | 21 | | internal class BasicRetryPolicy : ServiceBusRetryPolicy |
| | 22 | | { |
| | 23 | | /// <summary>The seed to use for initializing random number generated for a given thread-specific instance.</sum |
| 2 | 24 | | private static int s_randomSeed = Environment.TickCount; |
| | 25 | |
|
| | 26 | | /// <summary>The random number generator to use for a specific thread.</summary> |
| 16 | 27 | | private static readonly ThreadLocal<Random> RandomNumberGenerator = new ThreadLocal<Random>(() => new Random(Int |
| | 28 | |
|
| | 29 | | /// <summary> |
| | 30 | | /// The set of options responsible for configuring the retry |
| | 31 | | /// behavior. |
| | 32 | | /// </summary> |
| | 33 | | /// |
| 234 | 34 | | public ServiceBusRetryOptions Options { get; } |
| | 35 | |
|
| | 36 | | /// <summary> |
| | 37 | | /// The factor to apply to the base delay for use as a base jitter value. |
| | 38 | | /// </summary> |
| | 39 | | /// |
| | 40 | | /// <value>This factor is used as the basis for random jitter to apply to the calculated retry duration.</value> |
| | 41 | | /// |
| 124 | 42 | | public double JitterFactor { get; } = 0.08; |
| | 43 | |
|
| | 44 | | /// <summary> |
| | 45 | | /// Initializes a new instance of the <see cref="BasicRetryPolicy"/> class. |
| | 46 | | /// </summary> |
| | 47 | | /// |
| | 48 | | /// <param name="retryOptions">The options which control the retry approach.</param> |
| | 49 | | /// |
| 106 | 50 | | public BasicRetryPolicy(ServiceBusRetryOptions retryOptions) |
| | 51 | | { |
| 106 | 52 | | Argument.AssertNotNull(retryOptions, nameof(retryOptions)); |
| 106 | 53 | | Options = retryOptions; |
| 106 | 54 | | } |
| | 55 | |
|
| | 56 | | /// <summary> |
| | 57 | | /// Calculates the amount of time to allow the current attempt for an operation to |
| | 58 | | /// complete before considering it to be timed out. |
| | 59 | | /// </summary> |
| | 60 | | /// |
| | 61 | | /// <param name="attemptCount">The number of total attempts that have been made, including the initial attempt b |
| | 62 | | /// |
| | 63 | | /// <returns>The amount of time to allow for an operation to complete.</returns> |
| | 64 | | /// |
| 36 | 65 | | public override TimeSpan CalculateTryTimeout(int attemptCount) => Options.TryTimeout; |
| | 66 | |
|
| | 67 | | /// <summary> |
| | 68 | | /// Calculates the amount of time to wait before another attempt should be made. |
| | 69 | | /// </summary> |
| | 70 | | /// |
| | 71 | | /// <param name="lastException">The last exception that was observed for the operation to be retried.</param> |
| | 72 | | /// <param name="attemptCount">The number of total attempts that have been made, including the initial attempt b |
| | 73 | | /// |
| | 74 | | /// <returns>The amount of time to delay before retrying the associated operation; if <c>null</c>, then the oper |
| | 75 | | /// |
| | 76 | | public override TimeSpan? CalculateRetryDelay( |
| | 77 | | Exception lastException, |
| | 78 | | int attemptCount) |
| | 79 | | { |
| 36 | 80 | | if ((Options.MaxRetries <= 0) |
| 36 | 81 | | || (Options.Delay == TimeSpan.Zero) |
| 36 | 82 | | || (Options.MaxDelay == TimeSpan.Zero) |
| 36 | 83 | | || (attemptCount > Options.MaxRetries) |
| 36 | 84 | | || (!ShouldRetryException(lastException))) |
| | 85 | | { |
| 18 | 86 | | return null; |
| | 87 | | } |
| | 88 | |
|
| 18 | 89 | | var baseJitterSeconds = (Options.Delay.TotalSeconds * JitterFactor); |
| | 90 | |
|
| 18 | 91 | | TimeSpan retryDelay = Options.Mode switch |
| 18 | 92 | | { |
| 36 | 93 | | ServiceBusRetryMode.Fixed => CalculateFixedDelay(Options.Delay.TotalSeconds, baseJitterSeconds, RandomNu |
| 0 | 94 | | ServiceBusRetryMode.Exponential => CalculateExponentialDelay(attemptCount, Options.Delay.TotalSeconds, b |
| 0 | 95 | | _ => throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, Resources.UnknownRetryMod |
| 18 | 96 | | }; |
| | 97 | |
|
| | 98 | | // Adjust the delay, if needed, to keep within the maximum |
| | 99 | | // duration. |
| | 100 | |
|
| 18 | 101 | | if (Options.MaxDelay < retryDelay) |
| | 102 | | { |
| 0 | 103 | | return Options.MaxDelay; |
| | 104 | | } |
| | 105 | |
|
| 18 | 106 | | return retryDelay; |
| | 107 | | } |
| | 108 | |
|
| | 109 | | /// <summary> |
| | 110 | | /// Determines if an exception should be retried. |
| | 111 | | /// </summary> |
| | 112 | | /// |
| | 113 | | /// <param name="exception">The exception to consider.</param> |
| | 114 | | /// |
| | 115 | | /// <returns><c>true</c> to retry the exception; otherwise, <c>false</c>.</returns> |
| | 116 | | /// |
| | 117 | | private static bool ShouldRetryException(Exception exception) |
| | 118 | | { |
| | 119 | | // There's there's an ambient transaction - should not retry |
| 24 | 120 | | if (Transaction.Current != null) |
| | 121 | | { |
| 0 | 122 | | return false; |
| | 123 | | } |
| | 124 | |
|
| 24 | 125 | | if ((exception is TaskCanceledException) || (exception is OperationCanceledException)) |
| | 126 | | { |
| 2 | 127 | | exception = exception?.InnerException; |
| | 128 | | } |
| 22 | 129 | | else if (exception is AggregateException aggregateEx) |
| | 130 | | { |
| 0 | 131 | | exception = aggregateEx?.Flatten().InnerException; |
| | 132 | | } |
| | 133 | |
|
| | 134 | | switch (exception) |
| | 135 | | { |
| | 136 | | case null: |
| 2 | 137 | | return false; |
| | 138 | |
|
| | 139 | | case ServiceBusException ex: |
| 18 | 140 | | return ex.IsTransient; |
| | 141 | |
|
| | 142 | | case TimeoutException _: |
| | 143 | | case SocketException _: |
| 0 | 144 | | return true; |
| | 145 | |
|
| | 146 | | default: |
| 4 | 147 | | return false; |
| | 148 | | } |
| | 149 | | } |
| | 150 | |
|
| | 151 | | /// <summary> |
| | 152 | | /// Calculates the delay for an exponential back-off. |
| | 153 | | /// </summary> |
| | 154 | | /// |
| | 155 | | /// <param name="attemptCount">The number of total attempts that have been made, including the initial attempt b |
| | 156 | | /// <param name="baseDelaySeconds">The delay to use as a basis for the exponential back-off, in seconds.</param> |
| | 157 | | /// <param name="baseJitterSeconds">The delay to use as the basis for a random jitter value, in seconds.</param> |
| | 158 | | /// <param name="random">The random number generator to use for the calculation.</param> |
| | 159 | | /// |
| | 160 | | /// <returns>The recommended duration to delay before retrying; this value does not take the maximum delay or el |
| | 161 | | /// |
| | 162 | | private static TimeSpan CalculateExponentialDelay( |
| | 163 | | int attemptCount, |
| | 164 | | double baseDelaySeconds, |
| | 165 | | double baseJitterSeconds, |
| | 166 | | Random random) => |
| 0 | 167 | | TimeSpan.FromSeconds((Math.Pow(2, attemptCount) * baseDelaySeconds) + (random.NextDouble() * baseJitterSecon |
| | 168 | |
|
| | 169 | | /// <summary> |
| | 170 | | /// Calculates the delay for a fixed back-off. |
| | 171 | | /// </summary> |
| | 172 | | /// |
| | 173 | | /// <param name="baseDelaySeconds">The delay to use as a basis for the fixed back-off, in seconds.</param> |
| | 174 | | /// <param name="baseJitterSeconds">The delay to use as the basis for a random jitter value, in seconds.</param> |
| | 175 | | /// <param name="random">The random number generator to use for the calculation.</param> |
| | 176 | | /// |
| | 177 | | /// <returns>The recommended duration to delay before retrying; this value does not take the maximum delay or el |
| | 178 | | /// |
| | 179 | | private static TimeSpan CalculateFixedDelay( |
| | 180 | | double baseDelaySeconds, |
| | 181 | | double baseJitterSeconds, |
| | 182 | | Random random) => |
| 18 | 183 | | TimeSpan.FromSeconds(baseDelaySeconds + (random.NextDouble() * baseJitterSeconds)); |
| | 184 | | } |
| | 185 | | } |