Skip to content

Commit

Permalink
WaitAndRetry/Forever: calc wait from error response (#367)
Browse files Browse the repository at this point in the history
WaitAndRetryForever: take error as input to sleepDurationProvider

* Allow WaitAndRetryForever to take error as input to sleepDurationProvider.
* Tidy up other WaitAndRetry overloads.
* More tests.
* Doco
  • Loading branch information
reisenberger authored Nov 20, 2017
1 parent cb4be80 commit 9da3f7d
Show file tree
Hide file tree
Showing 19 changed files with 592 additions and 237 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 5.6.0
- Bug fix: set context keys for generic execute method with PolicyWrap
- Add GetPolicies extension method to IPolicyWrap
- Allow WaitAndRetry policies to calculate wait based on the handled fault

## 5.5.0
- Bug fix: non-generic CachePolicy with PolicyWrap
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,8 @@ For details of changes by release see the [change log](https://github.com/App-vN
* [@Extremo75](https://github.com/ExtRemo75) - Allow fallback delegates to take handled fault as input parameter.
* [@reisenberger](https://github.com/reisenberger) and [@seanfarrow](https://github.com/SeanFarrow) - Add CachePolicy, with interfaces for pluggable cache providers and serializers.
* [@MartinSStewart](https://github.com/martinsstewart) - Add GetPolicies extension method to IPolicyWrap.
* [@matst80](https://github.com/matst80) - Allow WaitAndRetry to take handled fault as an input to the sleepDurationProvider, allowing WaitAndRetry to take account of systems which specify a duration to wait as part of a fault response; eg Azure Cosmos DB may specify this in `x-ms-retry-after-ms` headers or in a property to an exception thrown by the Azure SDK.
* [@reisenberger](https://github.com/reisenberger) - Allow WaitAndRetryForever to take handled fault as an input to the sleepDurationProvider.
# Sample Projects

Expand Down
1 change: 1 addition & 0 deletions src/Polly.Net40Async.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
---------------------
- Bug fix: set context keys for generic execute method with PolicyWrap
- Add GetPolicies extension method to IPolicyWrap
- Allow WaitAndRetry policies to calculate wait based on the handled fault

5.5.0
---------------------
Expand Down
13 changes: 7 additions & 6 deletions src/Polly.Shared/Retry/RetryStateWaitAndRetryForever.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ namespace Polly.Retry
internal partial class RetryStateWaitAndRetryForever<TResult> : IRetryPolicyState<TResult>
{
private int _errorCount;
private readonly Func<int, Context, TimeSpan> _sleepDurationProvider;
private readonly Func<int, DelegateResult<TResult>, Context, TimeSpan> _sleepDurationProvider;
private readonly Action<DelegateResult<TResult>, TimeSpan, Context> _onRetry;
private readonly Context _context;

public RetryStateWaitAndRetryForever(Func<int, Context, TimeSpan> sleepDurationProvider, Action<DelegateResult<TResult>, TimeSpan, Context> onRetry, Context context)
public RetryStateWaitAndRetryForever(Func<int, DelegateResult<TResult>, Context, TimeSpan> sleepDurationProvider, Action<DelegateResult<TResult>, TimeSpan, Context> onRetry, Context context)
{
_sleepDurationProvider = sleepDurationProvider;
_onRetry = onRetry;
Expand All @@ -23,12 +23,13 @@ public bool CanRetry(DelegateResult<TResult> delegateResult, CancellationToken c
if (_errorCount < int.MaxValue)
{
_errorCount += 1;
}
}

var currentTimeSpan = _sleepDurationProvider(_errorCount, _context);
_onRetry(delegateResult, currentTimeSpan, _context);
TimeSpan waitTimeSpan = _sleepDurationProvider(_errorCount, delegateResult, _context);

SystemClock.Sleep(currentTimeSpan, cancellationToken);
_onRetry(delegateResult, waitTimeSpan, _context);

SystemClock.Sleep(waitTimeSpan, cancellationToken);

return true;
}
Expand Down
9 changes: 5 additions & 4 deletions src/Polly.Shared/Retry/RetryStateWaitAndRetryForeverAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal partial class RetryStateWaitAndRetryForever<TResult> : IRetryPolicyStat
{
private readonly Func<DelegateResult<TResult>, TimeSpan, Context, Task> _onRetryAsync;

public RetryStateWaitAndRetryForever(Func<int, Context, TimeSpan> sleepDurationProvider, Func<DelegateResult<TResult>, TimeSpan, Context, Task> onRetryAsync, Context context)
public RetryStateWaitAndRetryForever(Func<int, DelegateResult<TResult>, Context, TimeSpan> sleepDurationProvider, Func<DelegateResult<TResult>, TimeSpan, Context, Task> onRetryAsync, Context context)
{
_sleepDurationProvider = sleepDurationProvider;
_onRetryAsync = onRetryAsync;
Expand All @@ -23,10 +23,11 @@ public async Task<bool> CanRetryAsync(DelegateResult<TResult> delegateResult, Ca
_errorCount += 1;
}

var currentTimeSpan = _sleepDurationProvider(_errorCount, _context);
await _onRetryAsync(delegateResult, currentTimeSpan, _context).ConfigureAwait(continueOnCapturedContext);
TimeSpan waitTimeSpan = _sleepDurationProvider(_errorCount, delegateResult, _context);

await SystemClock.SleepAsync(currentTimeSpan, cancellationToken).ConfigureAwait(continueOnCapturedContext);
await _onRetryAsync(delegateResult, waitTimeSpan, _context).ConfigureAwait(continueOnCapturedContext);

await SystemClock.SleepAsync(waitTimeSpan, cancellationToken).ConfigureAwait(continueOnCapturedContext);

return true;
}
Expand Down
70 changes: 26 additions & 44 deletions src/Polly.Shared/Retry/RetrySyntax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,46 +306,7 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int ret
);
}

///// <summary>
///// Builds a <see cref="Policy"/> that will wait and retry <paramref name="retryCount"/> times
///// calling <paramref name="onRetry"/> on each retry with the raised exception, current sleep duration, retry count, and context data.
///// On each retry, the duration to wait is calculated by calling <paramref name="sleepDurationProvider"/> with
///// the current retry attempt allowing an exponentially increasing wait time (exponential backoff).
///// </summary>
///// <param name="policyBuilder">The policy builder.</param>
///// <param name="retryCount">The retry count.</param>
///// <param name="sleepDurationProvider">The function that provides the duration to wait for for a particular retry attempt.</param>
///// <param name="onRetry">The action to call on each retry.</param>
///// <returns>The policy instance.</returns>
///// <exception cref="System.ArgumentOutOfRangeException">retryCount;Value must be greater than or equal to zero.</exception>
///// <exception cref="System.ArgumentNullException">
///// timeSpanProvider
///// or
///// onRetry
///// </exception>
//public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func<int, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, int, Context> onRetry)
//{
// if (retryCount < 0) throw new ArgumentOutOfRangeException(nameof(retryCount), "Value must be greater than or equal to zero.");
// if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
// if (onRetry == null) throw new ArgumentNullException(nameof(onRetry));

// return new RetryPolicy(
// (action, context, cancellationToken) => RetryEngine.Implementation(
// (ctx, ct) => { action(ctx, ct); return EmptyStruct.Instance; },
// context,
// cancellationToken,
// policyBuilder.ExceptionPredicates,
// PredicateHelper<EmptyStruct>.EmptyResultPredicates,
// () => new RetryStateWaitAndRetryWithProvider<EmptyStruct>(
// retryCount,
// sleepDurationProvider,
// (outcome, timespan, i, ctx) => onRetry(outcome.Exception, timespan, i, ctx),
// context)
// ), policyBuilder.ExceptionPredicates);
//}

// For v560waitDurationFromErrorResponse: delete the overload above, and replace with the two below


/// <summary>
/// Builds a <see cref="Policy"/> that will wait and retry <paramref name="retryCount"/> times
/// calling <paramref name="onRetry"/> on each retry with the raised exception, current sleep duration, retry count, and context data.
Expand All @@ -365,6 +326,7 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int ret
/// </exception>
public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func<int, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, int, Context> onRetry)
{
if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
return policyBuilder.WaitAndRetry(
retryCount,
(i, outcome, ctx) => sleepDurationProvider(i, ctx),
Expand Down Expand Up @@ -394,8 +356,6 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int ret
if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
if (onRetry == null) throw new ArgumentNullException(nameof(onRetry));

//var wrappedEmptyStruct = new Action<DelegateResult<EmptyStruct>, TimeSpan, int, Context>()

return new RetryPolicy(
(action, context, cancellationToken) => RetryEngine.Implementation(
(ctx, ct) => { action(ctx, ct); return EmptyStruct.Instance; },
Expand Down Expand Up @@ -562,17 +522,39 @@ public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder,
/// <exception cref="System.ArgumentNullException">sleepDurationProvider</exception>
/// <exception cref="System.ArgumentNullException">onRetry</exception>
public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func<int, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry)
{
if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
return policyBuilder.WaitAndRetryForever(
(i, outcome, ctx) => sleepDurationProvider(i, ctx),
onRetry
);
}

/// <summary>
/// Builds a <see cref="Policy"/> that will wait and retry indefinitely until the action succeeds,
/// calling <paramref name="onRetry"/> on each retry with the raised exception and
/// execution context.
/// </summary>
/// <param name="policyBuilder">The policy builder.</param>
/// <param name="sleepDurationProvider">A function providing the duration to wait before retrying.</param>
/// <param name="onRetry">The action to call on each retry.</param>
/// <returns>The policy instance.</returns>
/// <exception cref="System.ArgumentNullException">sleepDurationProvider</exception>
/// <exception cref="System.ArgumentNullException">onRetry</exception>
public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func<int, Exception, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, Context> onRetry)
{
if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
if (onRetry == null) throw new ArgumentNullException(nameof(onRetry));

return new RetryPolicy((action, context, cancellationToken) => RetryEngine.Implementation(
(ctx, ct) => { action(ctx, ct); return EmptyStruct.Instance; },
context,
context,
cancellationToken,
policyBuilder.ExceptionPredicates,
PredicateHelper<EmptyStruct>.EmptyResultPredicates,
() => new RetryStateWaitAndRetryForever<EmptyStruct>(sleepDurationProvider, (outcome, timespan, ctx) => onRetry(outcome.Exception, timespan, ctx), context)
() => new RetryStateWaitAndRetryForever<EmptyStruct>(
(i, outcome, ctx) => sleepDurationProvider(i, outcome.Exception, ctx),
(outcome, timespan, ctx) => onRetry(outcome.Exception, timespan, ctx), context)
), policyBuilder.ExceptionPredicates);
}
}
Expand Down
70 changes: 27 additions & 43 deletions src/Polly.Shared/Retry/RetrySyntaxAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -565,46 +565,6 @@ public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, in
);
}

///// <summary>
///// Builds a <see cref="Policy" /> that will wait and retry <paramref name="retryCount" /> times
///// calling <paramref name="onRetryAsync" /> on each retry with the raised exception, the current sleep duration, retry count, and context data.
///// On each retry, the duration to wait is calculated by calling <paramref name="sleepDurationProvider" /> with
///// the current retry attempt allowing an exponentially increasing wait time (exponential backoff).
///// </summary>
///// <param name="policyBuilder">The policy builder.</param>
///// <param name="retryCount">The retry count.</param>
///// <param name="sleepDurationProvider">The function that provides the duration to wait for for a particular retry attempt.</param>
///// <param name="onRetryAsync">The action to call asynchronously on each retry.</param>
///// <returns>The policy instance.</returns>
///// <exception cref="System.ArgumentOutOfRangeException">retryCount;Value must be greater than or equal to zero.</exception>
///// <exception cref="System.ArgumentNullException">
///// sleepDurationProvider
///// or
///// onRetryAsync
///// </exception>
//public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
// Func<int, Context, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, int, Context, Task> onRetryAsync)
//{
// if (retryCount < 0) throw new ArgumentOutOfRangeException(nameof(retryCount), "Value must be greater than or equal to zero.");
// if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
// if (onRetryAsync == null) throw new ArgumentNullException(nameof(onRetryAsync));

// return new RetryPolicy(
// (action, context, cancellationToken, continueOnCapturedContext) =>
// RetryEngine.ImplementationAsync(
// async (ctx, ct) => { await action(ctx, ct).ConfigureAwait(continueOnCapturedContext); return EmptyStruct.Instance; },
// context,
// cancellationToken,
// policyBuilder.ExceptionPredicates,
// PredicateHelper<EmptyStruct>.EmptyResultPredicates,
// () => new RetryStateWaitAndRetryWithProvider<EmptyStruct>(retryCount, sleepDurationProvider, (outcome, timespan, i, ctx) => onRetryAsync(outcome.Exception, timespan, i, ctx), context),
// continueOnCapturedContext),
// policyBuilder.ExceptionPredicates
// );
//}

// For v560waitDurationFromErrorResponse: delete the overload above, and replace with the two below

/// <summary>
/// Builds a <see cref="Policy" /> that will wait and retry <paramref name="retryCount" /> times
/// calling <paramref name="onRetryAsync" /> on each retry with the raised exception, the current sleep duration, retry count, and context data.
Expand All @@ -625,6 +585,7 @@ public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, in
public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount,
Func<int, Context, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, int, Context, Task> onRetryAsync)
{
if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
return policyBuilder.WaitAndRetryAsync(
retryCount,
(i, outcome, ctx) => sleepDurationProvider(i, ctx),
Expand Down Expand Up @@ -964,6 +925,26 @@ public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuil
/// <exception cref="System.ArgumentNullException">sleepDurationProvider</exception>
/// <exception cref="System.ArgumentNullException">onRetryAsync</exception>
public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func<int, Context, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, Context, Task> onRetryAsync)
{
if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
return policyBuilder.WaitAndRetryForeverAsync(
(i, outcome, ctx) => sleepDurationProvider(i, ctx),
onRetryAsync
);
}

/// <summary>
/// Builds a <see cref="Policy"/> that will wait and retry indefinitely
/// calling <paramref name="onRetryAsync"/> on each retry with the raised exception and
/// execution context.
/// </summary>
/// <param name="policyBuilder">The policy builder.</param>
/// <param name="sleepDurationProvider">A function providing the duration to wait before retrying.</param>
/// <param name="onRetryAsync">The action to call asynchronously on each retry.</param>
/// <returns>The policy instance.</returns>
/// <exception cref="System.ArgumentNullException">sleepDurationProvider</exception>
/// <exception cref="System.ArgumentNullException">onRetryAsync</exception>
public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func<int, Exception, Context, TimeSpan> sleepDurationProvider, Func<Exception, TimeSpan, Context, Task> onRetryAsync)
{
if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider));
if (onRetryAsync == null) throw new ArgumentNullException(nameof(onRetryAsync));
Expand All @@ -976,9 +957,12 @@ public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuil
cancellationToken,
policyBuilder.ExceptionPredicates,
PredicateHelper<EmptyStruct>.EmptyResultPredicates,
() => new RetryStateWaitAndRetryForever<EmptyStruct>(sleepDurationProvider, (outcome, timespan, ctx) => onRetryAsync(outcome.Exception, timespan, ctx), context),
continueOnCapturedContext
), policyBuilder.ExceptionPredicates);
() => new RetryStateWaitAndRetryForever<EmptyStruct>(
(i, outcome, ctx) => sleepDurationProvider(i, outcome.Exception, ctx),
(outcome, timespan, ctx) => onRetryAsync(outcome.Exception, timespan, ctx),
context),
continueOnCapturedContext
), policyBuilder.ExceptionPredicates);
}
}
}
Expand Down
Loading

0 comments on commit 9da3f7d

Please sign in to comment.