Skip to content

Commit

Permalink
Document infinite retries capability. Max delay between retries incre…
Browse files Browse the repository at this point in the history
…ased to larger limit in .NET ≥ 6.
  • Loading branch information
Aldaviva committed Aug 7, 2023
1 parent 93f8366 commit 8e6821e
Show file tree
Hide file tree
Showing 5 changed files with 22 additions and 19 deletions.
4 changes: 2 additions & 2 deletions Readme.md
Expand Up @@ -159,8 +159,8 @@ Retrier.Attempt(attempt => MyErrorProneAction(), maxAttempts: 2);

1. Call **`Retrier.Attempt()`**. Pass
1. **`Action<int> action`/`Func<int, T> func`** — your delegate to attempt, and possibly retry if it throws exceptions. The attempt number will be passed as the `int` parameter, starting with `0` before the first attempt. If this func returns a `Task`, it will be awaited to determine if it threw an exception.
1. **`int maxAttempts`** — the maximum number of times the delegate may be executed, including the initial attempt. Optional, defaults to `2`. Must be at least `1`.
1. **`Func<int, TimeSpan> delay`** — how long to wait between attempts, as a function of the attempt number. The upcoming attempt number will be passed as a parameter, starting with `1` before the second attempt. You can return a constant `TimeSpan` for a fixed delay, or pass longer values for subsequent attempts to implement, for example, exponential backoff. Optional, defaults to no delay.
1. **`int maxAttempts`** — the total number of times the delegate is allowed to run in this invocation, equal to `1` initial attempt plus up to `maxAttempts - 1` retries if it throws an exception. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. For infinite retries, pass `null`.
1. **`Func<int, TimeSpan> delay`** — how long to wait between attempts, as a function of the attempt number. The upcoming attempt number will be passed as a parameter, starting with `1` before the second attempt. You can return a constant `TimeSpan` for a fixed delay, or pass longer values for subsequent attempts to implement, for example, exponential backoff. Optional, defaults to `null`, which means no delay. The minimum value is `0`, the maximum value is `int.MaxValue` (`uint.MaxValue - 1` starting in .NET 6), and values outside this range will be clipped.
1. **`Func<Exception, bool> isRetryAllowed`** — whether the delegate is permitted to execute again after a given `Exception` instance. Return `true` to allow or `false` to deny retries. For example, you may want to retry after HTTP 500 errors since subsequent requests may succeed, but stop after the first failure for an HTTP 403 error which probably won't succeed if the same request is sent again. Optional, defaults to retrying on all exceptions besides `OutOfMemoryException`.
1. **`Action beforeRetry`/`Func<Task> beforeRetry`** — a delegate to run extra logic between attempts, for example, if you want to log a message or perform any cleanup before the next attempt. Optional, defaults to not running anything between attempts.
1. **`CancellationToken cancellationToken`** — used to cancel the attempts and delays before they have all completed. Optional, defaults to no cancellation token. When cancelled, `Attempt()` throws a `TaskCancelledException`.
Expand Down
10 changes: 5 additions & 5 deletions Tests/Tests.csproj
Expand Up @@ -6,15 +6,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="FluentAssertions" Version="6.11.0" />
<!-- Keep Microsoft.NET.Test.Sdk at 16 so that it can handle the .NET Framework 4 target of the code under test. -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
<PackageReference Include="xunit" Version="2.5.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
3 changes: 2 additions & 1 deletion ThrottleDebounce/RateLimiter.cs
Expand Up @@ -4,7 +4,7 @@
using System.Threading;
using Timer = System.Timers.Timer;

namespace ThrottleDebounce;
namespace ThrottleDebounce;

internal partial class RateLimiter<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult> {

Expand All @@ -19,6 +19,7 @@ internal partial class RateLimiter<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11,
private TResult? mostRecentResult;
private bool disposed;

/// <exception cref="ArgumentException">if <paramref name="leading"/> and <paramref name="trailing"/> were both <see langword="false"/>, or if <paramref name="maxWait"/> is non-positive</exception>
internal RateLimiter(Delegate rateLimitedCallback, TimeSpan wait, bool leading, bool trailing, TimeSpan maxWait = default) {
if (!leading && !trailing) {
throw new ArgumentException("One or both of the leading and trailing arguments must be true, but both were false.");
Expand Down
20 changes: 11 additions & 9 deletions ThrottleDebounce/Retrier.cs
Expand Up @@ -11,14 +11,16 @@ namespace ThrottleDebounce;
/// </summary>
public static class Retrier {

private static readonly TimeSpan MaxDelay = TimeSpan.FromMilliseconds(int.MaxValue);
// ExceptionAdjustment: M:System.TimeSpan.FromMilliseconds(System.Double) -T:System.OverflowException
// These are the specific maximum values that don't overflow, tailored for the runtime version.
private static readonly TimeSpan MaxDelay = TimeSpan.FromMilliseconds(Environment.Version.Major >= 6 ? uint.MaxValue - 1 : int.MaxValue);

/// <summary>
/// Run the given <paramref name="action"/> at most <paramref name="maxAttempts"/> times, until it returns without throwing an exception.
/// </summary>
/// <param name="action">An action which is prone to sometimes throw exceptions. The <see cref="int"/> argument is the number of attempts, starting from <c>0</c> for the initial attempt.</param>
/// <param name="maxAttempts">The total number of times <paramref name="action"/> is allowed to run in this invocation. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. This is equal to 1 initial attempt plus <c>maxAttempts-1</c> retries if it throws an exception.</param>
/// <param name="delay">How long to wait between attempts. Defaults to no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0 ms, int.MaxValue ms]</c> will be clipped.</param>
/// <param name="maxAttempts">The total number of times <paramref name="action"/> is allowed to run in this invocation, equal to <c>1</c> initial attempt plus up to <c>maxAttempts - 1</c> retries if it throws an exception. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. For infinite retries, pass <see langword="null"/>.</param>
/// <param name="delay">How long to wait between attempts. Defaults to <see langword="null"/>, which means no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0, int.MaxValue]</c> ms will be clipped (<c>[0, uint.MaxValue-1]</c> ms starting in .NET 6).</param>
/// <param name="isRetryAllowed">Allows certain exceptions that indicate permanent failures to not trigger retries. For example, <see cref="ArgumentOutOfRangeException"/> will usually be thrown every time you call a function with the same arguments, so there is no reason to retry, and <paramref name="isRetryAllowed"/> could return <c>false</c> in that case. Defaults to retrying on every exception besides <see cref="OutOfMemoryException"/>.</param>
/// <param name="beforeRetry">Action to run between attempts, possibly to clean up some state before the next retry. For example, you may want to disconnect a failed connection before reconnecting. Defaults to no action.</param>
/// <param name="cancellationToken">Allows you to cancel remaining attempts and delays.</param>
Expand Down Expand Up @@ -58,8 +60,8 @@ public static class Retrier {
/// Run the given <paramref name="func"/> at most <paramref name="maxAttempts"/> times, until it returns without throwing an exception.
/// </summary>
/// <param name="func">An action which is prone to sometimes throw exceptions. The <see cref="int"/> argument is the number of attempts, starting from <c>0</c> for the initial attempt.</param>
/// <param name="maxAttempts">The total number of times <paramref name="func"/> is allowed to run in this invocation. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. This is equal to 1 initial attempt plus <c>maxAttempts-1</c> retries if it throws an exception.</param>
/// <param name="delay">How long to wait between attempts. Defaults to no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0 ms, int.MaxValue ms]</c> will be clipped.</param>
/// <param name="maxAttempts">The total number of times <paramref name="func"/> is allowed to run in this invocation, equal to <c>1</c> initial attempt plus up to <c>maxAttempts - 1</c> retries if it throws an exception. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. For infinite retries, pass <see langword="null"/>.</param>
/// <param name="delay">How long to wait between attempts. Defaults to <see langword="null"/>, which means no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0, int.MaxValue]</c> ms will be clipped (<c>[0, uint.MaxValue-1]</c> ms starting in .NET 6).</param>
/// <param name="isRetryAllowed">Allows certain exceptions that indicate permanent failures to not trigger retries. For example, <see cref="ArgumentOutOfRangeException"/> will usually be thrown every time you call a function with the same arguments, so there is no reason to retry, and <paramref name="isRetryAllowed"/> could return <c>false</c> in that case. Defaults to retrying on every exception besides <see cref="OutOfMemoryException"/>.</param>
/// <param name="beforeRetry">Action to run between attempts, possibly to clean up some state before the next retry. For example, you may want to disconnect a failed connection before reconnecting. Defaults to no action.</param>
/// <param name="cancellationToken">Allows you to cancel remaining attempts and delays.</param>
Expand Down Expand Up @@ -97,8 +99,8 @@ public static class Retrier {
/// Run the given <paramref name="func"/> at most <paramref name="maxAttempts"/> times, until it returns without throwing an exception.
/// </summary>
/// <param name="func">An action which is prone to sometimes throw exceptions. The <see cref="int"/> argument is the number of attempts, starting from <c>0</c> for the initial attempt.</param>
/// <param name="maxAttempts">The total number of times <paramref name="func"/> is allowed to run in this invocation. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. This is equal to 1 initial attempt plus <c>maxAttempts-1</c> retries if it throws an exception.</param>
/// <param name="delay">How long to wait between attempts. Defaults to no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0 ms, int.MaxValue ms]</c> will be clipped.</param>
/// <param name="maxAttempts">The total number of times <paramref name="func"/> is allowed to run in this invocation, equal to <c>1</c> initial attempt plus up to <c>maxAttempts - 1</c> retries if it throws an exception. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. For infinite retries, pass <see langword="null"/>.</param>
/// <param name="delay">How long to wait between attempts. Defaults to <see langword="null"/>, which means no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0, int.MaxValue]</c> ms will be clipped (<c>[0, uint.MaxValue-1]</c> ms starting in .NET 6).</param>
/// <param name="isRetryAllowed">Allows certain exceptions that indicate permanent failures to not trigger retries. For example, <see cref="ArgumentOutOfRangeException"/> will usually be thrown every time you call a function with the same arguments, so there is no reason to retry, and <paramref name="isRetryAllowed"/> could return <c>false</c> in that case. Defaults to retrying on every exception besides <see cref="OutOfMemoryException"/>.</param>
/// <param name="beforeRetry">Action to run between attempts, possibly to clean up some state before the next retry. For example, you may want to disconnect a failed connection before reconnecting. Defaults to no action.</param>
/// <param name="cancellationToken">Allows you to cancel remaining attempts and delays.</param>
Expand Down Expand Up @@ -137,8 +139,8 @@ public static class Retrier {
/// Run the given <paramref name="func"/> at most <paramref name="maxAttempts"/> times, until it returns without throwing an exception.
/// </summary>
/// <param name="func">An action which is prone to sometimes throw exceptions. The <see cref="int"/> argument is the number of attempts, starting from <c>0</c> for the initial attempt.</param>
/// <param name="maxAttempts">The total number of times <paramref name="func"/> is allowed to run in this invocation. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. This is equal to 1 initial attempt plus <c>maxAttempts-1</c> retries if it throws an exception.</param>
/// <param name="delay">How long to wait between attempts. Defaults to no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0 ms, int.MaxValue ms]</c> will be clipped.</param>
/// <param name="maxAttempts">The total number of times <paramref name="func"/> is allowed to run in this invocation, equal to <c>1</c> initial attempt plus up to <c>maxAttempts - 1</c> retries if it throws an exception. Must be at least 1, if you pass 0 it will clip to 1. Defaults to 2. For infinite retries, pass <see langword="null"/>.</param>
/// <param name="delay">How long to wait between attempts. Defaults to <see langword="null"/>, which means no delay. This is a function of how many attempts have already failed (starting from <c>1</c>), to allow for strategies such as exponential back-off. Values outside the range <c>[0, int.MaxValue]</c> ms will be clipped (<c>[0, uint.MaxValue-1]</c> ms starting in .NET 6).</param>
/// <param name="isRetryAllowed">Allows certain exceptions that indicate permanent failures to not trigger retries. For example, <see cref="ArgumentOutOfRangeException"/> will usually be thrown every time you call a function with the same arguments, so there is no reason to retry, and <paramref name="isRetryAllowed"/> could return <c>false</c> in that case. Defaults to retrying on every exception besides <see cref="OutOfMemoryException"/>.</param>
/// <param name="beforeRetry">Action to run between attempts, possibly to clean up some state before the next retry. For example, you may want to disconnect a failed connection before reconnecting. Defaults to no action.</param>
/// <param name="cancellationToken">Allows you to cancel remaining attempts and delays.</param>
Expand Down
4 changes: 2 additions & 2 deletions ThrottleDebounce/ThrottleDebounce.csproj
Expand Up @@ -7,9 +7,9 @@
<Authors>Ben Hutchison</Authors>
<Company>Ben Hutchison</Company>
<Product>ThrottleDebounce</Product>
<Version>2.0.0</Version>
<Version>2.0.1</Version>
<Description>Rate-limit your actions and funcs by throttling and debouncing them. Retry when an exception is thrown.</Description>
<Copyright2022 Ben Hutchison</Copyright>
<Copyright2023 Ben Hutchison</Copyright>
<PackageProjectUrl>https://github.com/Aldaviva/ThrottleDebounce</PackageProjectUrl>
<RepositoryUrl>https://github.com/Aldaviva/ThrottleDebounce.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
Expand Down

0 comments on commit 8e6821e

Please sign in to comment.