|
1 | 1 | using System.Reflection; |
2 | | -using System.Security.Cryptography; |
3 | 2 | using Vulthil.Messaging.Abstractions.Consumers; |
4 | 3 |
|
5 | 4 | namespace Vulthil.Messaging.Queues; |
@@ -79,263 +78,3 @@ public interface IQueueConfigurator |
79 | 78 | /// </summary> |
80 | 79 | IQueueConfigurator UseSingleActiveConsumer(); |
81 | 80 | } |
82 | | - |
83 | | -/// <summary> |
84 | | -/// Defines a retry policy including intervals, jitter, and exception filtering. |
85 | | -/// </summary> |
86 | | -public sealed record RetryPolicyDefinition |
87 | | -{ |
88 | | - /// <summary> |
89 | | - /// Gets or sets the maximum number of retry attempts. |
90 | | - /// </summary> |
91 | | - public int MaxRetryCount { get; set; } |
92 | | - /// <summary> |
93 | | - /// Gets or sets the jitter factor (0.0–1.0) applied to retry intervals. |
94 | | - /// </summary> |
95 | | - public double JitterFactor { get; set; } |
96 | | - /// <summary> |
97 | | - /// Gets or sets a value indicating whether retries run in-memory — the consumer is re-invoked in-process |
98 | | - /// while the delivery is held — rather than via delayed re-delivery through the retry queue. In-memory |
99 | | - /// retries preserve message order (a later message cannot overtake the one being retried), so they are |
100 | | - /// used automatically for partitioned queues. Defaults to <see langword="false"/> (delayed re-delivery). |
101 | | - /// </summary> |
102 | | - public bool InMemory { get; set; } |
103 | | - /// <summary> |
104 | | - /// Gets the delay intervals between successive retry attempts. |
105 | | - /// </summary> |
106 | | - public ICollection<TimeSpan> Intervals { get; } = []; |
107 | | - /// <summary> |
108 | | - /// Gets the fully-qualified type names of exceptions that should be excluded from retry attempts. |
109 | | - /// Names outside the core library must be assembly-qualified to resolve; a name that cannot be |
110 | | - /// resolved is skipped (the RabbitMQ transport logs a startup warning for each such name). |
111 | | - /// </summary> |
112 | | - public ICollection<string> IgnoreExceptions { get; } = []; |
113 | | - |
114 | | - private readonly object _ignoredTypesGate = new(); |
115 | | - private readonly HashSet<Type> _ignoredTypes = []; |
116 | | - private HashSet<Type>? _resolvedIgnoredTypes; |
117 | | - |
118 | | - internal void AddIgnoredType(Type type) => _ignoredTypes.Add(type); |
119 | | - /// <summary> |
120 | | - /// Gets the resolved CLR exception types that should be excluded from retry attempts. The set is built |
121 | | - /// once, on first access, from the fluently ignored types and the resolvable names in |
122 | | - /// <see cref="IgnoreExceptions"/>; the resolution is thread-safe and later mutations of either |
123 | | - /// collection do not change the result. |
124 | | - /// </summary> |
125 | | - public HashSet<Type> GetIgnoredExceptionTypes() |
126 | | - { |
127 | | - lock (_ignoredTypesGate) |
128 | | - { |
129 | | - return _resolvedIgnoredTypes ??= ResolveIgnoredExceptionTypes(); |
130 | | - } |
131 | | - } |
132 | | - |
133 | | - private HashSet<Type> ResolveIgnoredExceptionTypes() |
134 | | - { |
135 | | - var resolved = new HashSet<Type>(_ignoredTypes); |
136 | | - foreach (var name in IgnoreExceptions) |
137 | | - { |
138 | | - if (Type.GetType(name, false) is { } type) |
139 | | - { |
140 | | - resolved.Add(type); |
141 | | - } |
142 | | - } |
143 | | - |
144 | | - return resolved; |
145 | | - } |
146 | | - |
147 | | - /// <summary> |
148 | | - /// Calculates the delay for the specified retry attempt, applying jitter when configured. |
149 | | - /// Attempts beyond the last configured interval reuse that interval (capped back-off). |
150 | | - /// </summary> |
151 | | - /// <param name="attempt">The zero-based attempt index.</param> |
152 | | - /// <returns>The delay before the next retry.</returns> |
153 | | - public TimeSpan GetDelay(int attempt) |
154 | | - { |
155 | | - if (Intervals.Count == 0) |
156 | | - { |
157 | | - return TimeSpan.Zero; |
158 | | - } |
159 | | - |
160 | | - var interval = GetIntervalForAttempt(attempt); |
161 | | - if (JitterFactor <= 0.0) |
162 | | - { |
163 | | - return interval; |
164 | | - } |
165 | | - |
166 | | - var jitterMultiplier = RandomNumberGeneratorExtensions.GetDouble() * 2 * JitterFactor - JitterFactor; |
167 | | - var finalDelay = interval.TotalMilliseconds * (1 + jitterMultiplier); |
168 | | - return TimeSpan.FromMilliseconds(Math.Max(0, finalDelay)); |
169 | | - } |
170 | | - |
171 | | - private TimeSpan GetIntervalForAttempt(int attempt) |
172 | | - { |
173 | | - var index = Math.Min(attempt, Intervals.Count - 1); |
174 | | - return Intervals is IList<TimeSpan> list ? list[index] : Intervals.ElementAt(index); |
175 | | - } |
176 | | -} |
177 | | - |
178 | | -internal static class RandomNumberGeneratorExtensions |
179 | | -{ |
180 | | - /// <summary> |
181 | | - /// Generates a cryptographically random <see langword="double"/> in the range [0, 1). Retry jitter does |
182 | | - /// not need cryptographic strength, but the repository enforces CA5394 (no <see cref="Random"/>) as an |
183 | | - /// error, so the secure generator is used here as well. |
184 | | - /// </summary> |
185 | | - public static double GetDouble() |
186 | | - { |
187 | | - var bytes = RandomNumberGenerator.GetBytes(8); |
188 | | - var ul = BitConverter.ToUInt64(bytes, 0) / (1 << 11); |
189 | | - return ul / (double)(1UL << 53); |
190 | | - } |
191 | | -} |
192 | | - |
193 | | -/// <summary> |
194 | | -/// Describes the dead letter queue and exchange configuration. |
195 | | -/// </summary> |
196 | | -public sealed record DeadLetterDefinition |
197 | | -{ |
198 | | - /// <summary> |
199 | | - /// Gets or sets the dead letter queue name, or <see langword="null"/> to use the default. |
200 | | - /// </summary> |
201 | | - public string? QueueName { get; set; } |
202 | | - /// <summary> |
203 | | - /// Gets or sets the dead letter exchange name, or <see langword="null"/> to use the default. |
204 | | - /// </summary> |
205 | | - public string? ExchangeName { get; set; } |
206 | | - /// <summary> |
207 | | - /// Gets or sets a value indicating whether dead letter routing is enabled. |
208 | | - /// </summary> |
209 | | - public bool Enabled { get; set; } |
210 | | -} |
211 | | - |
212 | | -/// <summary> |
213 | | -/// Fluent builder for constructing a <see cref="RetryPolicyDefinition"/>. |
214 | | -/// </summary> |
215 | | -public sealed class RetryPolicyConfigurator |
216 | | -{ |
217 | | - private readonly HashSet<Type> _ignoredExceptions = []; |
218 | | - private List<TimeSpan> _intervals = []; |
219 | | - /// <summary> |
220 | | - /// Gets the maximum number of retry attempts configured via one of the interval methods. |
221 | | - /// </summary> |
222 | | - public int RetryLimit { get; private set; } |
223 | | - private double _jitterFactor; |
224 | | - private bool _inMemory; |
225 | | - /// <summary> |
226 | | - /// Gets the configured delay intervals between successive retry attempts. |
227 | | - /// </summary> |
228 | | - public IReadOnlyList<TimeSpan> Intervals => _intervals; |
229 | | - /// <summary> |
230 | | - /// Gets the exception types that should be excluded from retry attempts and immediately surfaced. |
231 | | - /// </summary> |
232 | | - public IReadOnlySet<Type> IgnoredExceptions => _ignoredExceptions; |
233 | | - |
234 | | - /// <summary> |
235 | | - /// Configures immediate retries with zero delay. |
236 | | - /// </summary> |
237 | | - /// <param name="retryCount">The number of retries.</param> |
238 | | - public void Immediate(int retryCount) |
239 | | - { |
240 | | - RetryLimit = retryCount; |
241 | | - _intervals = [.. Enumerable.Repeat(TimeSpan.Zero, retryCount)]; |
242 | | - } |
243 | | - |
244 | | - /// <summary> |
245 | | - /// Configures explicit retry intervals. |
246 | | - /// </summary> |
247 | | - /// <param name="intervals">The delay between each retry attempt.</param> |
248 | | - public void SetIntervals(params TimeSpan[] intervals) |
249 | | - { |
250 | | - RetryLimit = intervals.Length; |
251 | | - _intervals = [.. intervals]; |
252 | | - } |
253 | | - /// <summary> |
254 | | - /// Adds random jitter to retry intervals. |
255 | | - /// </summary> |
256 | | - /// <param name="factor">The jitter factor between 0.0 and 1.0.</param> |
257 | | - /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="factor"/> is outside the valid range.</exception> |
258 | | - public void UseJitter(double factor) |
259 | | - { |
260 | | - if (factor is < 0 or > 1.0) |
261 | | - { |
262 | | - throw new ArgumentOutOfRangeException(nameof(factor), "Jitter must be between 0.0 and 1.0"); |
263 | | - } |
264 | | - |
265 | | - _jitterFactor = factor; |
266 | | - } |
267 | | - /// <summary> |
268 | | - /// Configures exponential back-off retry intervals. |
269 | | - /// </summary> |
270 | | - /// <param name="retryCount">The number of retries.</param> |
271 | | - /// <param name="initialInterval">The delay for the first retry.</param> |
272 | | - /// <param name="maxInterval">The maximum delay cap.</param> |
273 | | - /// <param name="multiplier">The multiplier applied to each successive interval. Default is 2.0.</param> |
274 | | - public void Exponential( |
275 | | - int retryCount, |
276 | | - TimeSpan initialInterval, |
277 | | - TimeSpan maxInterval, |
278 | | - double multiplier = 2.0) |
279 | | - { |
280 | | - RetryLimit = retryCount; |
281 | | - var intervals = new List<TimeSpan>(); |
282 | | - var currentInterval = initialInterval; |
283 | | - |
284 | | - for (int i = 0; i < retryCount; i++) |
285 | | - { |
286 | | - intervals.Add(currentInterval); |
287 | | - |
288 | | - // Calculate next interval |
289 | | - double nextTicks = currentInterval.Ticks * multiplier; |
290 | | - currentInterval = TimeSpan.FromTicks((long)nextTicks); |
291 | | - |
292 | | - // Cap it at the max interval |
293 | | - if (currentInterval > maxInterval) |
294 | | - { |
295 | | - currentInterval = maxInterval; |
296 | | - } |
297 | | - } |
298 | | - |
299 | | - _intervals = [.. intervals]; |
300 | | - } |
301 | | - |
302 | | - /// <summary> |
303 | | - /// Adds an exception type to the ignore list; matching exceptions will not trigger retries. |
304 | | - /// </summary> |
305 | | - /// <typeparam name="TException">The exception type to ignore.</typeparam> |
306 | | - public void Ignore<TException>() where TException : Exception => _ignoredExceptions.Add(typeof(TException)); |
307 | | - |
308 | | - /// <summary> |
309 | | - /// Configures retries to run in-memory: the consumer is re-invoked in-process while the delivery is held, |
310 | | - /// instead of being re-delivered later through the retry queue. This preserves message order, so it is the |
311 | | - /// behavior used automatically for partitioned queues. |
312 | | - /// </summary> |
313 | | - public void InMemory() => _inMemory = true; |
314 | | - |
315 | | - /// <summary> |
316 | | - /// Builds the <see cref="RetryPolicyDefinition"/> from the current configuration. |
317 | | - /// </summary> |
318 | | - /// <returns>The constructed retry policy definition.</returns> |
319 | | - public RetryPolicyDefinition Build() |
320 | | - { |
321 | | - var definition = new RetryPolicyDefinition |
322 | | - { |
323 | | - MaxRetryCount = RetryLimit, |
324 | | - JitterFactor = _jitterFactor, |
325 | | - InMemory = _inMemory |
326 | | - }; |
327 | | - |
328 | | - foreach (var interval in _intervals) |
329 | | - { |
330 | | - definition.Intervals.Add(interval); |
331 | | - } |
332 | | - |
333 | | - foreach (var type in _ignoredExceptions) |
334 | | - { |
335 | | - definition.AddIgnoredType(type); |
336 | | - definition.IgnoreExceptions.Add(type.AssemblyQualifiedName!); |
337 | | - } |
338 | | - |
339 | | - return definition; |
340 | | - } |
341 | | -} |
0 commit comments