diff --git a/.gitignore b/.gitignore index bc5b2f67095..eae22ccb4ce 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ TestResults *.user *.sln.docstates .vs/ +.vscode/ # Build results [Dd]ebug/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a7af2322fd..bbb021db522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 5.6.0 +- Add ability to handle inner exceptions natively: .HandleInner<TEx>() +- Allow WaitAndRetry policies to calculate wait based on the handled fault +- Add the ability to access the policies within an IPolicyWrap +- Allow PolicyWrap to configure policies expressed as interfaces +- Bug fix: set context keys for generic execute methods with PolicyWrap +- Bug fix: generic TResult method with non-generic fallback policy +- Performance improvements +- Multiple build speed improvements + ## 5.5.0 - Bug fix: non-generic CachePolicy with PolicyWrap - Add Cache interfaces diff --git a/GitVersionConfig.yaml b/GitVersionConfig.yaml index 67dd25cf0fa..1cd752a447b 100644 --- a/GitVersionConfig.yaml +++ b/GitVersionConfig.yaml @@ -1 +1 @@ -next-version: 5.5.0 \ No newline at end of file +next-version: 5.6.0 \ No newline at end of file diff --git a/README.md b/README.md index c7e3cb1228c..f172e7b3359 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ You can install the Strongly Named version via: Install-Package Polly.Net40Async-Signed - # Resilience policies Polly offers multiple resilience policies: @@ -52,8 +51,6 @@ Fault-handling policies handle specific exceptions thrown by, or results returne ### (for fault-handling policies: Retry family, CircuitBreaker family and Fallback) - - ```csharp // Single exception type Policy @@ -72,6 +69,11 @@ Policy Policy .Handle(ex => ex.Number == 1205) .Or(ex => ex.ParamName == "example") + +// Inner exceptions of ordinary exceptions or AggregateException, with or without conditions +Policy + .HandleInner() + .OrInner(ex => ex.CancellationToken == myToken) ``` ## Step 1b: (optionally) Specify return results you want to handle @@ -938,6 +940,13 @@ For details of changes by release see the [change log](https://github.com/App-vN * [@jiimaho](https://github.com/jiimaho) and [@Extremo75](https://github.com/ExtRemo75) - Provide public factory methods for PolicyResult, to support testing. * [@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. +* Thanks to the awesome devs at [@tretton37](https://github.com/tretton37) who delivered the following as part of a one-day in-company hackathon led by [@reisenberger](https://github.com/reisenberger), sponsored by [@tretton37](https://github.com/tretton37) and convened by [@thecodejunkie](https://github.com/thecodejunkie) + * [@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 CosmosDB may specify this in `x-ms-retry-after-ms` headers or in a property to an exception thrown by the Azure CosmosDB SDK. + * [@MartinSStewart](https://github.com/martinsstewart) - Add GetPolicies() extension methods to IPolicyWrap. + * [@jbergens37](https://github.com/jbergens37) - Parallelize test running where possible, to improve overall build speed. +* [@reisenberger](https://github.com/reisenberger) - Add new .HandleInner(...) syntax for handling inner exceptions natively. +* [@rjongeneelen](https://github.com/rjongeneelen) and [@reisenberger](https://github.com/reisenberger) - Allow PolicyWrap configuration to configure policies via interfaces. +* [@reisenberger](https://github.com/reisenberger) - Performance improvements. # Sample Projects diff --git a/src/Polly.Net40Async.Specs/Properties/AssemblyInfo.cs b/src/Polly.Net40Async.Specs/Properties/AssemblyInfo.cs index 8b42c39f80f..d88b899ce3a 100644 --- a/src/Polly.Net40Async.Specs/Properties/AssemblyInfo.cs +++ b/src/Polly.Net40Async.Specs/Properties/AssemblyInfo.cs @@ -2,4 +2,4 @@ using Xunit; [assembly: AssemblyTitle("Polly.Net40Async.Specs")] -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(DisableTestParallelization = false)] \ No newline at end of file diff --git a/src/Polly.Net40Async.nuspec b/src/Polly.Net40Async.nuspec index 43f906e46a4..8272eb9c611 100644 --- a/src/Polly.Net40Async.nuspec +++ b/src/Polly.Net40Async.nuspec @@ -13,7 +13,17 @@ Exception Handling Resilience Transient Fault Policy Circuit Breaker CircuitBreaker Retry Wait Cache Cache-aside Bulkhead Fallback Timeout Throttle Parallelization Copyright © 2017, App vNext - v5.0 is a major release with significant new resilience policies: Timeout; Bulkhead Isolation; Fallback; Cache; and PolicyWrap. See release notes back to v5.0.0 for full details. v5.0.5 includes important circuit-breaker fixes. + v5.0 is a major release with significant new resilience policies: Timeout; Bulkhead Isolation; Fallback; Cache; and PolicyWrap. See release notes back to v5.0.0 for full details. + + 5.6.0 + --------------------- + - Add ability to handle inner exceptions natively: .HandleInner<TEx>() + - Allow WaitAndRetry policies to calculate wait based on the handled fault + - Add the ability to access the policies within an IPolicyWrap + - Allow PolicyWrap to take interfaces as parameters + - Bug fix: set context keys for generic execute method with PolicyWrap + - Bug fix: generic TResult method with non-generic fallback policy + - Performance improvements 5.5.0 --------------------- diff --git a/src/Polly.Net45.Specs/Properties/AssemblyInfo.cs b/src/Polly.Net45.Specs/Properties/AssemblyInfo.cs index cb2585b40f4..c35c2d67142 100644 --- a/src/Polly.Net45.Specs/Properties/AssemblyInfo.cs +++ b/src/Polly.Net45.Specs/Properties/AssemblyInfo.cs @@ -2,4 +2,4 @@ using Xunit; [assembly: AssemblyTitle("Polly.Net45.Specs")] -[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: CollectionBehavior(DisableTestParallelization = false)] \ No newline at end of file diff --git a/src/Polly.NetStandard11.Specs/Properties/AssemblyInfo.cs b/src/Polly.NetStandard11.Specs/Properties/AssemblyInfo.cs index 3969a68e84c..e76dbc82f26 100644 --- a/src/Polly.NetStandard11.Specs/Properties/AssemblyInfo.cs +++ b/src/Polly.NetStandard11.Specs/Properties/AssemblyInfo.cs @@ -1 +1 @@ -[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file +[assembly: Xunit.CollectionBehavior(DisableTestParallelization = false)] \ No newline at end of file diff --git a/src/Polly.NetStandard11/Properties/AssemblyInfo.cs b/src/Polly.NetStandard11/Properties/AssemblyInfo.cs index 4cfa91f815f..4041936b7ab 100644 --- a/src/Polly.NetStandard11/Properties/AssemblyInfo.cs +++ b/src/Polly.NetStandard11/Properties/AssemblyInfo.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("Polly")] -[assembly: AssemblyVersion("5.5.0.0")] +[assembly: AssemblyVersion("5.6.0.0")] [assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("Polly.NetStandard11.Specs")] \ No newline at end of file diff --git a/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngine.cs b/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngine.cs index 1e7c2dcc867..08ac52c24a9 100644 --- a/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngine.cs +++ b/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngine.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; +#if NET40 +using ExceptionDispatchInfo = Polly.Utilities.ExceptionDispatchInfo; +#endif + namespace Polly.CircuitBreaker { internal partial class CircuitBreakerEngine @@ -36,13 +41,20 @@ internal static TResult Implementation( } catch (Exception ex) { - if (!shouldHandleExceptionPredicates.Any(predicate => predicate(ex))) + Exception handledException = shouldHandleExceptionPredicates + .Select(predicate => predicate(ex)) + .FirstOrDefault(e => e != null); + if (handledException == null) { throw; } - breakerController.OnActionFailure(new DelegateResult(ex), context); + breakerController.OnActionFailure(new DelegateResult(handledException), context); + if (handledException != ex) + { + ExceptionDispatchInfo.Capture(handledException).Throw(); + } throw; } } diff --git a/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngineAsync.cs b/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngineAsync.cs index f6a56b58948..feb08e2aab8 100644 --- a/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngineAsync.cs +++ b/src/Polly.Shared/CircuitBreaker/CircuitBreakerEngineAsync.cs @@ -1,11 +1,14 @@ - - -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; +#if NET40 +using ExceptionDispatchInfo = Polly.Utilities.ExceptionDispatchInfo; +#endif + namespace Polly.CircuitBreaker { internal partial class CircuitBreakerEngine @@ -40,13 +43,20 @@ internal static async Task ImplementationAsync( } catch (Exception ex) { - if (!shouldHandleExceptionPredicates.Any(predicate => predicate(ex))) + Exception handledException = shouldHandleExceptionPredicates + .Select(predicate => predicate(ex)) + .FirstOrDefault(e => e != null); + if (handledException == null) { throw; } - breakerController.OnActionFailure(new DelegateResult(ex), context); + breakerController.OnActionFailure(new DelegateResult(handledException), context); + if (handledException != ex) + { + ExceptionDispatchInfo.Capture(handledException).Throw(); + } throw; } diff --git a/src/Polly.Shared/Context.Dictionary.cs b/src/Polly.Shared/Context.Dictionary.cs new file mode 100644 index 00000000000..ee51ab2feeb --- /dev/null +++ b/src/Polly.Shared/Context.Dictionary.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Polly +{ + /// + /// Context that carries with a single execution through a Policy. Commonly-used properties are directly on the class. Backed by a dictionary of string key / object value pairs, to which user-defined values may be added. + /// Do not re-use an instance of across more than one execution. + /// + public partial class Context : IDictionary, IDictionary +#if !NET40 + , IReadOnlyDictionary +#endif + { + // For an individual execution through a policy or policywrap, it is expected that all execution steps (for example executing the user delegate, invoking policy-activity delegates such as onRetry, onBreak, onTimeout etc) execute sequentially. + // Therefore, this class is intentionally not constructed to be safe for concurrent access from multiple threads. + + private Dictionary wrappedDictionary = null; + + private Dictionary WrappedDictionary => wrappedDictionary ?? (wrappedDictionary = new Dictionary()); + + /// + /// Initializes a new instance of the class, with the specified and the supplied . + /// + /// The execution key. + /// The context data. + public Context(String executionKey, IDictionary contextData) : this(contextData) + { + ExecutionKey = executionKey; + } + + internal Context(IDictionary contextData) : this() + { + if (contextData == null) throw new ArgumentNullException(nameof(contextData)); + wrappedDictionary = new Dictionary(contextData); + } + +#region IDictionary implementation + + /// + public ICollection Keys => WrappedDictionary.Keys; + + /// + public ICollection Values => WrappedDictionary.Values; + + /// + public int Count => WrappedDictionary.Count; + + /// + bool ICollection>.IsReadOnly => ((IDictionary)WrappedDictionary).IsReadOnly; + + /// + public object this[string key] + { + get => WrappedDictionary[key]; + set => WrappedDictionary[key] = value; + } + + /// + public void Add(string key, object value) + { + WrappedDictionary.Add(key, value); + } + + /// + public bool ContainsKey(string key) => WrappedDictionary.ContainsKey(key); + + /// + public bool Remove(string key) => WrappedDictionary.Remove(key); + + /// + public bool TryGetValue(string key, out object value) => WrappedDictionary.TryGetValue(key, out value); + + /// + void ICollection>.Add(KeyValuePair item) => ((IDictionary)WrappedDictionary).Add(item); + + /// + public void Clear() => WrappedDictionary.Clear(); + + /// + bool ICollection>.Contains(KeyValuePair item) => ((IDictionary)WrappedDictionary).Contains(item); + + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => ((IDictionary) WrappedDictionary).CopyTo(array, arrayIndex); + + /// + bool ICollection>.Remove(KeyValuePair item) => ((IDictionary)WrappedDictionary).Remove(item); + + /// + public IEnumerator> GetEnumerator() => WrappedDictionary.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => WrappedDictionary.GetEnumerator(); + + /// + public void Add(object key, object value) + { + ((IDictionary)WrappedDictionary).Add(key, value); + } + + /// + public bool Contains(object key) + { + return ((IDictionary)WrappedDictionary).Contains(key); + } + + /// + IDictionaryEnumerator IDictionary.GetEnumerator() + { + return ((IDictionary)WrappedDictionary).GetEnumerator(); + } + + /// + public void Remove(object key) + { + ((IDictionary)WrappedDictionary).Remove(key); + } + + /// + public void CopyTo(Array array, int index) + { + ((IDictionary)WrappedDictionary).CopyTo(array, index); + } + + #endregion + +#if !NET40 + #region IReadOnlyDictionary implementation + IEnumerable IReadOnlyDictionary.Keys => ((IReadOnlyDictionary)WrappedDictionary).Keys; + + IEnumerable IReadOnlyDictionary.Values => ((IReadOnlyDictionary)WrappedDictionary).Values; + #endregion +#endif + + #region IDictionary implementation + + /// + bool IDictionary.IsFixedSize => ((IDictionary)WrappedDictionary).IsFixedSize; + + /// + bool IDictionary.IsReadOnly => ((IDictionary)WrappedDictionary).IsReadOnly; + + ICollection IDictionary.Keys => ((IDictionary)WrappedDictionary).Keys; + + ICollection IDictionary.Values => ((IDictionary)WrappedDictionary).Values; + + /// + bool ICollection.IsSynchronized => ((IDictionary)WrappedDictionary).IsSynchronized; + + /// + object ICollection.SyncRoot => ((IDictionary)WrappedDictionary).SyncRoot; + + /// + object IDictionary.this[object key] { get => ((IDictionary)WrappedDictionary)[key]; set => ((IDictionary)WrappedDictionary)[key] = value; } + +#endregion + } +} \ No newline at end of file diff --git a/src/Polly.Shared/Context.cs b/src/Polly.Shared/Context.cs index 104eb6637c4..0efb7b0180c 100644 --- a/src/Polly.Shared/Context.cs +++ b/src/Polly.Shared/Context.cs @@ -7,13 +7,9 @@ namespace Polly /// Context that carries with a single execution through a Policy. Commonly-used properties are directly on the class. Backed by a dictionary of string key / object value pairs, to which user-defined values may be added. /// Do not re-use an instance of across more than one execution. /// - public class Context : Dictionary + public partial class Context { - // For an individual execution through a policy or policywrap, it is expected that all execution steps (for example executing the user delegate, invoking policy-activity delegates such as onRetry, onBreak, onTimeout etc) execute sequentially. - // Therefore, this class is intentionally not constructed to be safe for concurrent access from multiple threads. - - private static readonly IDictionary emptyDictionary = new Dictionary(); - internal static readonly Context None = new Context(emptyDictionary); + internal static readonly Context None = new Context(); private Guid? _executionGuid; @@ -21,27 +17,13 @@ public class Context : Dictionary /// Initializes a new instance of the class, with the specified . /// /// The execution key. - public Context(String executionKey) : this(executionKey, emptyDictionary) - { - } - - /// - /// Initializes a new instance of the class, with the specified and the supplied . - /// - /// The execution key. - /// The context data. - public Context(String executionKey, IDictionary contextData) : this(contextData) + public Context(String executionKey) { ExecutionKey = executionKey; } - internal Context() : this(emptyDictionary) - { - } - - internal Context(IDictionary contextData) : base(contextData) + internal Context() { - if (contextData == null) throw new ArgumentNullException(nameof(contextData)); } /// diff --git a/src/Polly.Shared/ExceptionPredicate.cs b/src/Polly.Shared/ExceptionPredicate.cs index 0e960ebc7c9..138abac68be 100644 --- a/src/Polly.Shared/ExceptionPredicate.cs +++ b/src/Polly.Shared/ExceptionPredicate.cs @@ -2,5 +2,5 @@ namespace Polly { - internal delegate bool ExceptionPredicate(Exception ex); + internal delegate Exception ExceptionPredicate(Exception ex); } \ No newline at end of file diff --git a/src/Polly.Shared/Fallback/FallbackEngine.cs b/src/Polly.Shared/Fallback/FallbackEngine.cs index 2f7f47b99c9..0863c02fe25 100644 --- a/src/Polly.Shared/Fallback/FallbackEngine.cs +++ b/src/Polly.Shared/Fallback/FallbackEngine.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; +#if NET40 +using ExceptionDispatchInfo = Polly.Utilities.ExceptionDispatchInfo; +#endif + namespace Polly.Fallback { internal static partial class FallbackEngine @@ -33,12 +38,15 @@ internal static TResult Implementation( } catch (Exception ex) { - if (!shouldHandleExceptionPredicates.Any(predicate => predicate(ex))) + Exception handledException = shouldHandleExceptionPredicates + .Select(predicate => predicate(ex)) + .FirstOrDefault(e => e != null); + if (handledException == null) { throw; } - delegateOutcome = new DelegateResult(ex); + delegateOutcome = new DelegateResult(handledException); } onFallback(delegateOutcome, context); diff --git a/src/Polly.Shared/Fallback/FallbackEngineAsync.cs b/src/Polly.Shared/Fallback/FallbackEngineAsync.cs index 051a344b3b4..f8357c9a89a 100644 --- a/src/Polly.Shared/Fallback/FallbackEngineAsync.cs +++ b/src/Polly.Shared/Fallback/FallbackEngineAsync.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; +#if NET40 +using ExceptionDispatchInfo = Polly.Utilities.ExceptionDispatchInfo; +#endif + namespace Polly.Fallback { internal static partial class FallbackEngine @@ -35,12 +40,15 @@ internal static async Task ImplementationAsync( } catch (Exception ex) { - if (!shouldHandleExceptionPredicates.Any(predicate => predicate(ex))) + Exception handledException = shouldHandleExceptionPredicates + .Select(predicate => predicate(ex)) + .FirstOrDefault(e => e != null); + if (handledException == null) { throw; } - delegateOutcome = new DelegateResult(ex); + delegateOutcome = new DelegateResult(handledException); } await onFallbackAsync(delegateOutcome, context).ConfigureAwait(continueOnCapturedContext); diff --git a/src/Polly.Shared/Fallback/FallbackPolicy.cs b/src/Polly.Shared/Fallback/FallbackPolicy.cs index 1ffb80d3ddd..0d9c1906325 100644 --- a/src/Polly.Shared/Fallback/FallbackPolicy.cs +++ b/src/Polly.Shared/Fallback/FallbackPolicy.cs @@ -13,6 +13,19 @@ internal FallbackPolicy(Action, Context, Canc : base(exceptionPolicy, exceptionPredicates) { } + + /// + /// Executes the specified action within the cache policy and returns the result. + /// + /// The type of the result. + /// The action to perform. + /// Execution context that is passed to the exception policy; defines the cache key to use in cache lookup. + /// The cancellation token. + /// The value returned by the action, or the cache. + public override TResult ExecuteInternal(Func action, Context context, CancellationToken cancellationToken) + { + throw new InvalidOperationException($"You have executed the generic .Execute<{nameof(TResult)}> method on a non-generic {nameof(FallbackPolicy)}. A non-generic {nameof(FallbackPolicy)} only defines a fallback action which returns void; it can never return a substitute {nameof(TResult)} value. To use {nameof(FallbackPolicy)} to provide fallback {nameof(TResult)} values you must define a generic fallback policy {nameof(FallbackPolicy)}<{nameof(TResult)}>. For example, define the policy as Policy<{nameof(TResult)}>.Handle.Fallback<{nameof(TResult)}>(/* some {nameof(TResult)} value or Func<..., {nameof(TResult)}> */);"); + } } /// diff --git a/src/Polly.Shared/Fallback/FallbackPolicyAsync.cs b/src/Polly.Shared/Fallback/FallbackPolicyAsync.cs index 5c49e0ec61e..93020c2050c 100644 --- a/src/Polly.Shared/Fallback/FallbackPolicyAsync.cs +++ b/src/Polly.Shared/Fallback/FallbackPolicyAsync.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -11,6 +12,22 @@ internal FallbackPolicy(Func, Context, Ca : base(asyncExceptionPolicy, exceptionPredicates) { } + + /// + /// Executes the specified asynchronous action within the policy and returns the result. + /// + /// The type of the result. + /// The action to perform. + /// Context data that is passed to the exception policy. + /// Whether to continue on a captured synchronization context. + /// A cancellation token which can be used to cancel the action. When a retry policy is in use, also cancels any further retries. + /// The value returned by the action + /// Please use asynchronous-defined policies when calling asynchronous ExecuteAsync (and similar) methods. + [DebuggerStepThrough] + public override Task ExecuteAsyncInternal(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) + { + throw new InvalidOperationException($"You have executed the generic .Execute<{nameof(TResult)}> method on a non-generic {nameof(FallbackPolicy)}. A non-generic {nameof(FallbackPolicy)} only defines a fallback action which returns void; it can never return a substitute {nameof(TResult)} value. To use {nameof(FallbackPolicy)} to provide fallback {nameof(TResult)} values you must define a generic fallback policy {nameof(FallbackPolicy)}<{nameof(TResult)}>. For example, define the policy as Policy<{nameof(TResult)}>.Handle.Fallback<{nameof(TResult)}>(/* some {nameof(TResult)} value or Func<..., {nameof(TResult)}> */);"); + } } public partial class FallbackPolicy : IFallbackPolicy diff --git a/src/Polly.Shared/Policy.HandleSyntax.cs b/src/Polly.Shared/Policy.HandleSyntax.cs index 4a1e1d7677b..e7e5b68b2c8 100644 --- a/src/Polly.Shared/Policy.HandleSyntax.cs +++ b/src/Polly.Shared/Policy.HandleSyntax.cs @@ -9,12 +9,10 @@ public partial class Policy /// Specifies the type of exception that this policy can handle. /// /// The type of the exception to handle. - /// The PolicyBuilder instance. + /// The PolicyBuilder instance, for fluent chaining. public static PolicyBuilder Handle() where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException; - - return new PolicyBuilder(predicate); + return new PolicyBuilder(exception => exception is TException ? exception : null); } /// @@ -22,13 +20,30 @@ public static PolicyBuilder Handle() where TException : Exception /// /// The type of the exception. /// The exception predicate to filter the type of exception this policy can handle. - /// The PolicyBuilder instance. + /// The PolicyBuilder instance, for fluent chaining. public static PolicyBuilder Handle(Func exceptionPredicate) where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException && - exceptionPredicate((TException)exception); + return new PolicyBuilder(exception => exception is TException texception && exceptionPredicate(texception) ? exception : null); + } - return new PolicyBuilder(predicate); + /// + /// Specifies the type of exception that this policy can handle if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public static PolicyBuilder HandleInner() where TException : Exception + { + return new PolicyBuilder(PolicyBuilder.HandleInner(ex => ex is TException)); + } + + /// + /// Specifies the type of exception that this policy can handle, with additional filters on this exception type, if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public static PolicyBuilder HandleInner(Func exceptionPredicate) where TException : Exception + { + return new PolicyBuilder(PolicyBuilder.HandleInner(ex => ex is TException texception && exceptionPredicate(texception))); } /// @@ -64,9 +79,7 @@ public partial class Policy /// The PolicyBuilder instance. public static PolicyBuilder Handle() where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException; - - return new PolicyBuilder(predicate); + return new PolicyBuilder(exception => exception is TException ? exception : null); } /// @@ -77,10 +90,27 @@ public static PolicyBuilder Handle() where TException : Exc /// The PolicyBuilder instance. public static PolicyBuilder Handle(Func exceptionPredicate) where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException && - exceptionPredicate((TException)exception); + return new PolicyBuilder(exception => exception is TException texception && exceptionPredicate(texception) ? exception : null); + } - return new PolicyBuilder(predicate); + /// + /// Specifies the type of exception that this policy can handle if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public static PolicyBuilder HandleInner() where TException : Exception + { + return new PolicyBuilder(PolicyBuilder.HandleInner(ex => ex is TException)); + } + + /// + /// Specifies the type of exception that this policy can handle, with additional filters on this exception type, if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public static PolicyBuilder HandleInner(Func exceptionPredicate) where TException : Exception + { + return new PolicyBuilder(PolicyBuilder.HandleInner(ex => ex is TException texception && exceptionPredicate(texception))); } /// diff --git a/src/Polly.Shared/Policy.TResult.cs b/src/Polly.Shared/Policy.TResult.cs index d5f4c291ff5..d9873a4de9b 100644 --- a/src/Polly.Shared/Policy.TResult.cs +++ b/src/Polly.Shared/Policy.TResult.cs @@ -22,9 +22,7 @@ internal Policy( IEnumerable> resultPredicates ) { - if (executionPolicy == null) throw new ArgumentNullException(nameof(executionPolicy)); - - _executionPolicy = executionPolicy; + _executionPolicy = executionPolicy ?? throw new ArgumentNullException(nameof(executionPolicy)); ExceptionPredicates = exceptionPredicates ?? PredicateHelper.EmptyExceptionPredicates; ResultPredicates = resultPredicates ?? PredicateHelper.EmptyResultPredicates; } diff --git a/src/Polly.Shared/Policy.cs b/src/Polly.Shared/Policy.cs index e107f2f27e5..a0ff568ad71 100644 --- a/src/Polly.Shared/Policy.cs +++ b/src/Polly.Shared/Policy.cs @@ -20,9 +20,7 @@ internal Policy( Action, Context, CancellationToken> exceptionPolicy, IEnumerable exceptionPredicates) { - if (exceptionPolicy == null) throw new ArgumentNullException(nameof(exceptionPolicy)); - - _exceptionPolicy = exceptionPolicy; + _exceptionPolicy = exceptionPolicy ?? throw new ArgumentNullException(nameof(exceptionPolicy)); ExceptionPredicates = exceptionPredicates ?? PredicateHelper.EmptyExceptionPredicates; } @@ -619,7 +617,7 @@ public PolicyResult ExecuteAndCapture(Func exceptionPredicates, Exception exception) { - var isExceptionTypeHandledByThisPolicy = exceptionPredicates.Any(predicate => predicate(exception)); + var isExceptionTypeHandledByThisPolicy = exceptionPredicates.Any(predicate => predicate(exception) != null); return isExceptionTypeHandledByThisPolicy ? ExceptionType.HandledByThisPolicy diff --git a/src/Polly.Shared/PolicyAsync.TResult.cs b/src/Polly.Shared/PolicyAsync.TResult.cs index e60643fe858..7320b36c277 100644 --- a/src/Polly.Shared/PolicyAsync.TResult.cs +++ b/src/Polly.Shared/PolicyAsync.TResult.cs @@ -17,9 +17,7 @@ internal Policy( IEnumerable exceptionPredicates, IEnumerable> resultPredicates) { - if (asyncExecutionPolicy == null) throw new ArgumentNullException(nameof(asyncExecutionPolicy)); - - _asyncExecutionPolicy = asyncExecutionPolicy; + _asyncExecutionPolicy = asyncExecutionPolicy ?? throw new ArgumentNullException(nameof(asyncExecutionPolicy)); ExceptionPredicates = exceptionPredicates ?? PredicateHelper.EmptyExceptionPredicates; ResultPredicates = resultPredicates ?? PredicateHelper.EmptyResultPredicates; } diff --git a/src/Polly.Shared/PolicyAsync.cs b/src/Polly.Shared/PolicyAsync.cs index b1b7dab1e0c..6e3a6153b18 100644 --- a/src/Polly.Shared/PolicyAsync.cs +++ b/src/Polly.Shared/PolicyAsync.cs @@ -15,9 +15,7 @@ internal Policy( Func, Context, CancellationToken, bool, Task> asyncExceptionPolicy, IEnumerable exceptionPredicates) { - if (asyncExceptionPolicy == null) throw new ArgumentNullException(nameof(asyncExceptionPolicy)); - - _asyncExceptionPolicy = asyncExceptionPolicy; + _asyncExceptionPolicy = asyncExceptionPolicy ?? throw new ArgumentNullException(nameof(asyncExceptionPolicy)); ExceptionPredicates = exceptionPredicates ?? PredicateHelper.EmptyExceptionPredicates; } diff --git a/src/Polly.Shared/PolicyBuilder.OrSyntax.cs b/src/Polly.Shared/PolicyBuilder.OrSyntax.cs index 94f4e786750..721bf0de18f 100644 --- a/src/Polly.Shared/PolicyBuilder.OrSyntax.cs +++ b/src/Polly.Shared/PolicyBuilder.OrSyntax.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using System.Text; +using System.Linq; namespace Polly { @@ -15,8 +14,7 @@ public partial class PolicyBuilder /// The PolicyBuilder instance. public PolicyBuilder Or() where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException; - ExceptionPredicates.Add(predicate); + ExceptionPredicates.Add(exception => exception is TException ? exception : null); return this; } @@ -28,13 +26,53 @@ public PolicyBuilder Or() where TException : Exception /// The PolicyBuilder instance. public PolicyBuilder Or(Func exceptionPredicate) where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException && - exceptionPredicate((TException) exception); + ExceptionPredicates.Add(exception => exception is TException texception && exceptionPredicate(texception) ? exception : null); + return this; + } + + /// + /// Specifies the type of exception that this policy can handle if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public PolicyBuilder OrInner() where TException : Exception + { + ExceptionPredicates.Add((HandleInner(ex => ex is TException))); + return this; + } - ExceptionPredicates.Add(predicate); + /// + /// Specifies the type of exception that this policy can handle, with additional filters on this exception type, if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public PolicyBuilder OrInner(Func exceptionPredicate) where TException : Exception + { + ExceptionPredicates.Add(HandleInner(exception => exception is TException texception && exceptionPredicate(texception))); return this; } + internal static ExceptionPredicate HandleInner(Func predicate) + { + return exception => + { + if (exception is AggregateException aggregateException) + { + Exception matchedInAggregate = aggregateException.Flatten().InnerExceptions.FirstOrDefault(predicate); + if (matchedInAggregate != null) return matchedInAggregate; + } + + return HandleInnerNested(predicate, exception); + }; + } + + private static Exception HandleInnerNested(Func predicate, Exception current) + { + if (current == null) return null; + if (predicate(current)) return current; + return HandleInnerNested(predicate, current.InnerException); + } + #endregion #region Add result predicates to exception-filtering policy @@ -103,8 +141,7 @@ public PolicyBuilder OrResult(TResult result) /// The PolicyBuilder instance. public PolicyBuilder Or() where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException; - ExceptionPredicates.Add(predicate); + ExceptionPredicates.Add(exception => exception is TException ? exception : null); return this; } @@ -116,10 +153,29 @@ public PolicyBuilder Or() where TException : Exception /// The PolicyBuilder instance. public PolicyBuilder Or(Func exceptionPredicate) where TException : Exception { - ExceptionPredicate predicate = exception => exception is TException && - exceptionPredicate((TException)exception); + ExceptionPredicates.Add(exception => exception is TException texception && exceptionPredicate(texception) ? exception : null); + return this; + } + + /// + /// Specifies the type of exception that this policy can handle if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public PolicyBuilder OrInner() where TException : Exception + { + ExceptionPredicates.Add((PolicyBuilder.HandleInner(ex => ex is TException))); + return this; + } - ExceptionPredicates.Add(predicate); + /// + /// Specifies the type of exception that this policy can handle, with additional filters on this exception type, if found as an InnerException of a regular , or at any level of nesting within an . + /// + /// The type of the exception to handle. + /// The PolicyBuilder instance, for fluent chaining. + public PolicyBuilder OrInner(Func exceptionPredicate) where TException : Exception + { + ExceptionPredicates.Add(PolicyBuilder.HandleInner(ex => ex is TException texception && exceptionPredicate(texception))); return this; } diff --git a/src/Polly.Shared/Polly.Shared.projitems b/src/Polly.Shared/Polly.Shared.projitems index 9372edbbf3c..294203281cb 100644 --- a/src/Polly.Shared/Polly.Shared.projitems +++ b/src/Polly.Shared/Polly.Shared.projitems @@ -70,6 +70,7 @@ + @@ -149,6 +150,7 @@ + diff --git a/src/Polly.Shared/Retry/RetryEngine.cs b/src/Polly.Shared/Retry/RetryEngine.cs index 1385bdbd514..d45dce87f43 100644 --- a/src/Polly.Shared/Retry/RetryEngine.cs +++ b/src/Polly.Shared/Retry/RetryEngine.cs @@ -1,8 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; +#if NET40 +using ExceptionDispatchInfo = Polly.Utilities.ExceptionDispatchInfo; +#endif + namespace Polly.Retry { internal static partial class RetryEngine @@ -37,13 +42,20 @@ internal static TResult Implementation( } catch (Exception ex) { - if (!shouldRetryExceptionPredicates.Any(predicate => predicate(ex))) + Exception handledException = shouldRetryExceptionPredicates + .Select(predicate => predicate(ex)) + .FirstOrDefault(e => e != null); + if (handledException == null) { throw; } - if (!policyState.CanRetry(new DelegateResult(ex), cancellationToken)) + if (!policyState.CanRetry(new DelegateResult(handledException), cancellationToken)) { + if (handledException != ex) + { + ExceptionDispatchInfo.Capture(handledException).Throw(); + } throw; } } diff --git a/src/Polly.Shared/Retry/RetryEngineAsync.cs b/src/Polly.Shared/Retry/RetryEngineAsync.cs index 186a81fd0fc..c3165ba452d 100644 --- a/src/Polly.Shared/Retry/RetryEngineAsync.cs +++ b/src/Polly.Shared/Retry/RetryEngineAsync.cs @@ -1,12 +1,14 @@ - - -using System; +using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; +#if NET40 +using ExceptionDispatchInfo = Polly.Utilities.ExceptionDispatchInfo; +#endif + namespace Polly.Retry { internal static partial class RetryEngine @@ -44,15 +46,22 @@ internal static async Task ImplementationAsync( } catch (Exception ex) { - if (!shouldRetryExceptionPredicates.Any(predicate => predicate(ex))) + Exception handledException = shouldRetryExceptionPredicates + .Select(predicate => predicate(ex)) + .FirstOrDefault(e => e != null); + if (handledException == null) { throw; } if (!await policyState - .CanRetryAsync(new DelegateResult(ex), cancellationToken, continueOnCapturedContext) + .CanRetryAsync(new DelegateResult(handledException), cancellationToken, continueOnCapturedContext) .ConfigureAwait(continueOnCapturedContext)) { + if (handledException != ex) + { + ExceptionDispatchInfo.Capture(handledException).Throw(); + } throw; } } diff --git a/src/Polly.Shared/Retry/RetryStateWaitAndRetryForever.cs b/src/Polly.Shared/Retry/RetryStateWaitAndRetryForever.cs index 091bb535460..2650fd4f5c1 100644 --- a/src/Polly.Shared/Retry/RetryStateWaitAndRetryForever.cs +++ b/src/Polly.Shared/Retry/RetryStateWaitAndRetryForever.cs @@ -7,11 +7,11 @@ namespace Polly.Retry internal partial class RetryStateWaitAndRetryForever : IRetryPolicyState { private int _errorCount; - private readonly Func _sleepDurationProvider; + private readonly Func, Context, TimeSpan> _sleepDurationProvider; private readonly Action, TimeSpan, Context> _onRetry; private readonly Context _context; - public RetryStateWaitAndRetryForever(Func sleepDurationProvider, Action, TimeSpan, Context> onRetry, Context context) + public RetryStateWaitAndRetryForever(Func, Context, TimeSpan> sleepDurationProvider, Action, TimeSpan, Context> onRetry, Context context) { _sleepDurationProvider = sleepDurationProvider; _onRetry = onRetry; @@ -23,12 +23,13 @@ public bool CanRetry(DelegateResult 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; } diff --git a/src/Polly.Shared/Retry/RetryStateWaitAndRetryForeverAsync.cs b/src/Polly.Shared/Retry/RetryStateWaitAndRetryForeverAsync.cs index f47b15272cd..bd8793cb107 100644 --- a/src/Polly.Shared/Retry/RetryStateWaitAndRetryForeverAsync.cs +++ b/src/Polly.Shared/Retry/RetryStateWaitAndRetryForeverAsync.cs @@ -9,7 +9,7 @@ internal partial class RetryStateWaitAndRetryForever : IRetryPolicyStat { private readonly Func, TimeSpan, Context, Task> _onRetryAsync; - public RetryStateWaitAndRetryForever(Func sleepDurationProvider, Func, TimeSpan, Context, Task> onRetryAsync, Context context) + public RetryStateWaitAndRetryForever(Func, Context, TimeSpan> sleepDurationProvider, Func, TimeSpan, Context, Task> onRetryAsync, Context context) { _sleepDurationProvider = sleepDurationProvider; _onRetryAsync = onRetryAsync; @@ -23,10 +23,11 @@ public async Task CanRetryAsync(DelegateResult 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; } diff --git a/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProvider.cs b/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProvider.cs index a360bb0899b..e6f226076a4 100644 --- a/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProvider.cs +++ b/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProvider.cs @@ -8,11 +8,11 @@ internal partial class RetryStateWaitAndRetryWithProvider : IRetryPolic { private int _errorCount; private readonly int _retryCount; - private readonly Func _sleepDurationProvider; + private readonly Func, Context, TimeSpan> _sleepDurationProvider; private readonly Action, TimeSpan, int, Context> _onRetry; private readonly Context _context; - public RetryStateWaitAndRetryWithProvider(int retryCount, Func sleepDurationProvider, Action, TimeSpan, int, Context> onRetry, Context context) + public RetryStateWaitAndRetryWithProvider(int retryCount, Func, Context, TimeSpan> sleepDurationProvider, Action, TimeSpan, int, Context> onRetry, Context context) { _retryCount = retryCount; _sleepDurationProvider = sleepDurationProvider; @@ -27,7 +27,7 @@ public bool CanRetry(DelegateResult delegateResult, CancellationToken c bool shouldRetry = _errorCount <= _retryCount; if (shouldRetry) { - TimeSpan waitTimeSpan = _sleepDurationProvider(_errorCount, _context); + TimeSpan waitTimeSpan = _sleepDurationProvider(_errorCount, delegateResult, _context); _onRetry(delegateResult, waitTimeSpan, _errorCount, _context); diff --git a/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProviderAsync.cs b/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProviderAsync.cs index 67dab2dbfe6..ca391277da8 100644 --- a/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProviderAsync.cs +++ b/src/Polly.Shared/Retry/RetryStateWaitAndRetryWithProviderAsync.cs @@ -10,7 +10,7 @@ internal partial class RetryStateWaitAndRetryWithProvider { private readonly Func, TimeSpan, int, Context, Task> _onRetryAsync; - public RetryStateWaitAndRetryWithProvider(int retryCount, Func sleepDurationProvider, Func, TimeSpan, int, Context, Task> onRetryAsync, Context context) + public RetryStateWaitAndRetryWithProvider(int retryCount, Func, Context, TimeSpan> sleepDurationProvider, Func, TimeSpan, int, Context, Task> onRetryAsync, Context context) { _retryCount = retryCount; _sleepDurationProvider = sleepDurationProvider; @@ -25,7 +25,7 @@ public async Task CanRetryAsync(DelegateResult delegateResult, Ca bool shouldRetry = _errorCount <= _retryCount; if (shouldRetry) { - TimeSpan waitTimeSpan = _sleepDurationProvider(_errorCount, _context); + TimeSpan waitTimeSpan = _sleepDurationProvider(_errorCount, delegateResult, _context); await _onRetryAsync(delegateResult, waitTimeSpan, _errorCount, _context).ConfigureAwait(continueOnCapturedContext); diff --git a/src/Polly.Shared/Retry/RetrySyntax.cs b/src/Polly.Shared/Retry/RetrySyntax.cs index d0c5bcccb1e..2738e997415 100644 --- a/src/Polly.Shared/Retry/RetrySyntax.cs +++ b/src/Polly.Shared/Retry/RetrySyntax.cs @@ -306,6 +306,7 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int ret ); } + /// /// Builds a that will wait and retry times /// calling on each retry with the raised exception, current sleep duration, retry count, and context data. @@ -324,6 +325,32 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int ret /// onRetry /// public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider, Action onRetry) + { + if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); + return policyBuilder.WaitAndRetry( + retryCount, + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetry); + } + + /// + /// Builds a that will wait and retry times + /// calling 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 with + /// the current retry attempt allowing an exponentially increasing wait time (exponential backoff). + /// + /// The policy builder. + /// The retry count. + /// The function that provides the duration to wait for for a particular retry attempt. + /// The action to call on each retry. + /// The policy instance. + /// retryCount;Value must be greater than or equal to zero. + /// + /// timeSpanProvider + /// or + /// onRetry + /// + public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider, Action 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)); @@ -336,7 +363,11 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int ret cancellationToken, policyBuilder.ExceptionPredicates, PredicateHelper.EmptyResultPredicates, - () => new RetryStateWaitAndRetryWithProvider(retryCount, sleepDurationProvider, (outcome, timespan, i, ctx) => onRetry(outcome.Exception, timespan, i, ctx), context) + () => new RetryStateWaitAndRetryWithProvider( + retryCount, + (i, outcome, ctx) => sleepDurationProvider(i, outcome.Exception, ctx), + (outcome, timespan, i, ctx) => onRetry(outcome.Exception, timespan, i, ctx), + context) ), policyBuilder.ExceptionPredicates); } @@ -491,17 +522,39 @@ public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, /// sleepDurationProvider /// onRetry public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func sleepDurationProvider, Action onRetry) + { + if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); + return policyBuilder.WaitAndRetryForever( + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetry + ); + } + + /// + /// Builds a that will wait and retry indefinitely until the action succeeds, + /// calling on each retry with the raised exception and + /// execution context. + /// + /// The policy builder. + /// A function providing the duration to wait before retrying. + /// The action to call on each retry. + /// The policy instance. + /// sleepDurationProvider + /// onRetry + public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func sleepDurationProvider, Action 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.EmptyResultPredicates, - () => new RetryStateWaitAndRetryForever(sleepDurationProvider, (outcome, timespan, ctx) => onRetry(outcome.Exception, timespan, ctx), context) + () => new RetryStateWaitAndRetryForever( + (i, outcome, ctx) => sleepDurationProvider(i, outcome.Exception, ctx), + (outcome, timespan, ctx) => onRetry(outcome.Exception, timespan, ctx), context) ), policyBuilder.ExceptionPredicates); } } diff --git a/src/Polly.Shared/Retry/RetrySyntaxAsync.cs b/src/Polly.Shared/Retry/RetrySyntaxAsync.cs index 2d4477e4151..21f9459ee1d 100644 --- a/src/Polly.Shared/Retry/RetrySyntaxAsync.cs +++ b/src/Polly.Shared/Retry/RetrySyntaxAsync.cs @@ -584,6 +584,33 @@ public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, in /// public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider, Func onRetryAsync) + { + if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); + return policyBuilder.WaitAndRetryAsync( + retryCount, + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetryAsync); + } + + /// + /// Builds a that will wait and retry times + /// calling 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 with + /// the current retry attempt allowing an exponentially increasing wait time (exponential backoff). + /// + /// The policy builder. + /// The retry count. + /// The function that provides the duration to wait for for a particular retry attempt. + /// The action to call asynchronously on each retry. + /// The policy instance. + /// retryCount;Value must be greater than or equal to zero. + /// + /// sleepDurationProvider + /// or + /// onRetryAsync + /// + public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, + Func sleepDurationProvider, Func 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)); @@ -597,7 +624,11 @@ public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, in cancellationToken, policyBuilder.ExceptionPredicates, PredicateHelper.EmptyResultPredicates, - () => new RetryStateWaitAndRetryWithProvider(retryCount, sleepDurationProvider, (outcome, timespan, i, ctx) => onRetryAsync(outcome.Exception, timespan, i, ctx), context), + () => new RetryStateWaitAndRetryWithProvider( + retryCount, + (i, outcome, ctx) => sleepDurationProvider(i, outcome.Exception, ctx), + (outcome, timespan, i, ctx) => onRetryAsync(outcome.Exception, timespan, i, ctx), + context), continueOnCapturedContext), policyBuilder.ExceptionPredicates ); @@ -894,6 +925,26 @@ public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuil /// sleepDurationProvider /// onRetryAsync public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func sleepDurationProvider, Func onRetryAsync) + { + if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); + return policyBuilder.WaitAndRetryForeverAsync( + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetryAsync + ); + } + + /// + /// Builds a that will wait and retry indefinitely + /// calling on each retry with the raised exception and + /// execution context. + /// + /// The policy builder. + /// A function providing the duration to wait before retrying. + /// The action to call asynchronously on each retry. + /// The policy instance. + /// sleepDurationProvider + /// onRetryAsync + public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func sleepDurationProvider, Func onRetryAsync) { if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); if (onRetryAsync == null) throw new ArgumentNullException(nameof(onRetryAsync)); @@ -906,9 +957,12 @@ public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuil cancellationToken, policyBuilder.ExceptionPredicates, PredicateHelper.EmptyResultPredicates, - () => new RetryStateWaitAndRetryForever(sleepDurationProvider, (outcome, timespan, ctx) => onRetryAsync(outcome.Exception, timespan, ctx), context), - continueOnCapturedContext - ), policyBuilder.ExceptionPredicates); + () => new RetryStateWaitAndRetryForever( + (i, outcome, ctx) => sleepDurationProvider(i, outcome.Exception, ctx), + (outcome, timespan, ctx) => onRetryAsync(outcome.Exception, timespan, ctx), + context), + continueOnCapturedContext + ), policyBuilder.ExceptionPredicates); } } } diff --git a/src/Polly.Shared/Retry/RetryTResultSyntax.cs b/src/Polly.Shared/Retry/RetryTResultSyntax.cs index d76e22066cb..1ade32df69f 100644 --- a/src/Polly.Shared/Retry/RetryTResultSyntax.cs +++ b/src/Polly.Shared/Retry/RetryTResultSyntax.cs @@ -330,6 +330,76 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider, Action, TimeSpan, int, Context> onRetry) + { + return policyBuilder.WaitAndRetry( + retryCount, + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetry + ); + } + + /// + /// Builds a that will wait and retry times. + /// On each retry, the duration to wait is calculated by calling with + /// the current retry attempt allowing an exponentially increasing wait time (exponential backoff). + /// + /// The policy builder. + /// The retry count. + /// The function that provides the duration to wait for for a particular retry attempt. + /// The policy instance. + public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func, Context, TimeSpan> sleepDurationProvider) + { + Action, TimeSpan, int, Context> doNothing = (_, __, ___, ____) => { }; + + return policyBuilder.WaitAndRetry(retryCount, sleepDurationProvider, doNothing); + } + + /// + /// Builds a that will wait and retry times + /// calling on each retry with the handled exception or result, current sleep duration and context data. + /// On each retry, the duration to wait is calculated by calling with + /// the current retry attempt allowing an exponentially increasing wait time (exponential backoff). + /// + /// The policy builder. + /// The retry count. + /// The function that provides the duration to wait for for a particular retry attempt. + /// The action to call on each retry. + /// The policy instance. + /// retryCount;Value must be greater than or equal to zero. + /// + /// sleepDurationProvider + /// or + /// onRetry + /// + public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func, Context, TimeSpan> sleepDurationProvider, Action, TimeSpan, Context> onRetry) + { + if (onRetry == null) throw new ArgumentNullException(nameof(onRetry)); + + return policyBuilder.WaitAndRetry( + retryCount, + sleepDurationProvider, + (outcome, span, i, ctx) => onRetry(outcome, span, ctx) + ); + } + + /// + /// Builds a that will wait and retry times + /// calling on each retry with the handled exception or result, current sleep duration, retry count, and context data. + /// On each retry, the duration to wait is calculated by calling with + /// the current retry attempt allowing an exponentially increasing wait time (exponential backoff). + /// + /// The policy builder. + /// The retry count. + /// The function that provides the duration to wait for for a particular retry attempt. + /// The action to call on each retry. + /// The policy instance. + /// retryCount;Value must be greater than or equal to zero. + /// + /// timeSpanProvider + /// or + /// onRetry + /// + public static RetryPolicy WaitAndRetry(this PolicyBuilder policyBuilder, int retryCount, Func, Context, TimeSpan> sleepDurationProvider, Action, 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)); @@ -344,8 +414,8 @@ public static RetryPolicy WaitAndRetry(this PolicyBuilder new RetryStateWaitAndRetryWithProvider(retryCount, sleepDurationProvider, onRetry, context) ), - policyBuilder.ExceptionPredicates, - policyBuilder.ResultPredicates); + policyBuilder.ExceptionPredicates, + policyBuilder.ResultPredicates); } /// @@ -502,6 +572,26 @@ public static RetryPolicy WaitAndRetryForever(this PolicyBuild /// sleepDurationProvider /// onRetry public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func sleepDurationProvider, Action, TimeSpan, Context> onRetry) + { + if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); + return policyBuilder.WaitAndRetryForever( + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetry + ); + } + + /// + /// Builds a that will wait and retry indefinitely until the action succeeds, + /// calling on each retry with the handled exception or result and + /// execution context. + /// + /// The policy builder. + /// A function providing the duration to wait before retrying. + /// The action to call on each retry. + /// The policy instance. + /// sleepDurationProvider + /// onRetry + public static RetryPolicy WaitAndRetryForever(this PolicyBuilder policyBuilder, Func, Context, TimeSpan> sleepDurationProvider, Action, TimeSpan, Context> onRetry) { if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); if (onRetry == null) throw new ArgumentNullException(nameof(onRetry)); @@ -514,7 +604,7 @@ public static RetryPolicy WaitAndRetryForever(this PolicyBuild policyBuilder.ExceptionPredicates, policyBuilder.ResultPredicates, () => new RetryStateWaitAndRetryForever(sleepDurationProvider, onRetry, context) - ), + ), policyBuilder.ExceptionPredicates, policyBuilder.ResultPredicates ); diff --git a/src/Polly.Shared/Retry/RetryTResultSyntaxAsync.cs b/src/Polly.Shared/Retry/RetryTResultSyntaxAsync.cs index e19141a67f4..8b826288dfe 100644 --- a/src/Polly.Shared/Retry/RetryTResultSyntaxAsync.cs +++ b/src/Polly.Shared/Retry/RetryTResultSyntaxAsync.cs @@ -587,6 +587,33 @@ public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder /// public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, Func sleepDurationProvider, Func, TimeSpan, int, Context, Task> onRetryAsync) + { + if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); + return policyBuilder.WaitAndRetryAsync( + retryCount, + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetryAsync); + } + + /// + /// Builds a that will wait and retry times + /// calling on each retry with the handled exception or result, the current sleep duration, retry count, and context data. + /// On each retry, the duration to wait is calculated by calling with + /// the current retry attempt allowing an exponentially increasing wait time (exponential backoff). + /// + /// The policy builder. + /// The retry count. + /// The function that provides the duration to wait for for a particular retry attempt. + /// The action to call asynchronously on each retry. + /// The policy instance. + /// retryCount;Value must be greater than or equal to zero. + /// + /// sleepDurationProvider + /// or + /// onRetryAsync + /// + public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder policyBuilder, int retryCount, + Func, Context, TimeSpan> sleepDurationProvider, Func, 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)); @@ -600,7 +627,7 @@ public static RetryPolicy WaitAndRetryAsync(this PolicyBuilder cancellationToken, policyBuilder.ExceptionPredicates, policyBuilder.ResultPredicates, - () => new RetryStateWaitAndRetryWithProvider(retryCount, sleepDurationProvider, onRetryAsync, context), + () => new RetryStateWaitAndRetryWithProvider(retryCount, sleepDurationProvider, onRetryAsync, context), continueOnCapturedContext), policyBuilder.ExceptionPredicates, policyBuilder.ResultPredicates @@ -898,21 +925,41 @@ public static RetryPolicy WaitAndRetryForeverAsync(this Policy /// sleepDurationProvider /// onRetryAsync public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func sleepDurationProvider, Func, TimeSpan, Context, Task> onRetryAsync) + { + if (sleepDurationProvider == null) throw new ArgumentNullException(nameof(sleepDurationProvider)); + return policyBuilder.WaitAndRetryForeverAsync( + (i, outcome, ctx) => sleepDurationProvider(i, ctx), + onRetryAsync + ); + } + + /// + /// Builds a that will wait and retry indefinitely until the action succeeds, + /// calling on each retry with the handled exception or result and + /// execution context. + /// + /// The policy builder. + /// A function providing the duration to wait before retrying. + /// The action to call asynchronously on each retry. + /// The policy instance. + /// sleepDurationProvider + /// onRetryAsync + public static RetryPolicy WaitAndRetryForeverAsync(this PolicyBuilder policyBuilder, Func, Context, TimeSpan> sleepDurationProvider, Func, TimeSpan, Context, Task> onRetryAsync) { 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( - action, - context, - cancellationToken, - policyBuilder.ExceptionPredicates, - policyBuilder.ResultPredicates, - () => new RetryStateWaitAndRetryForever(sleepDurationProvider, onRetryAsync, context), - continueOnCapturedContext - ), + RetryEngine.ImplementationAsync( + action, + context, + cancellationToken, + policyBuilder.ExceptionPredicates, + policyBuilder.ResultPredicates, + () => new RetryStateWaitAndRetryForever(sleepDurationProvider, onRetryAsync, context), + continueOnCapturedContext + ), policyBuilder.ExceptionPredicates, policyBuilder.ResultPredicates); } diff --git a/src/Polly.Shared/Wrap/IPolicyWrapExtension.cs b/src/Polly.Shared/Wrap/IPolicyWrapExtension.cs new file mode 100644 index 00000000000..9592861bec2 --- /dev/null +++ b/src/Polly.Shared/Wrap/IPolicyWrapExtension.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Polly.Wrap +{ + /// + /// Extension methods for IPolicyWrap. + /// + public static class IPolicyWrapExtension + { + /// + /// Returns all the policies in this , in Outer-to-Inner order. + /// + /// The for which to return policies. + /// An of all the policies in the wrap. + public static IEnumerable GetPolicies(this IPolicyWrap policyWrap) + { + var childPolicies = new[] { policyWrap.Outer, policyWrap.Inner }; + foreach (var childPolicy in childPolicies) + { + if (childPolicy is IPolicyWrap anotherWrap) + { + foreach (var policy in anotherWrap.GetPolicies()) + { + yield return policy; + } + } + else if (childPolicy != null) + { + yield return childPolicy; + } + } + } + + /// + /// Returns all the policies in this of type , in Outer-to-Inner order. + /// + /// The for which to return policies. + /// The type of policies to return. + /// An of all the policies of the given type. + public static IEnumerable GetPolicies(this IPolicyWrap policyWrap) + { + return policyWrap.GetPolicies().OfType(); + } + + /// + /// Returns all the policies in this of type matching the filter, in Outer-to-Inner order. + /// + /// The for which to return policies. + /// A filter to apply to any policies of type found. + /// The type of policies to return. + /// An of all the policies of the given type, matching the filter. + public static IEnumerable GetPolicies(this IPolicyWrap policyWrap, Func filter) + { + if (filter == null) throw new ArgumentNullException(nameof(filter)); + return policyWrap.GetPolicies().OfType().Where(filter); + } + + /// + /// Returns the single policy in this of type . + /// + /// The for which to search for the policy. + /// The type of policy to return. + /// A if one is found; else null. + /// InvalidOperationException, if more than one policy of the type is found in the wrap. + public static TPolicy GetPolicy(this IPolicyWrap policyWrap) + { + return policyWrap.GetPolicies().OfType().SingleOrDefault(); + } + + /// + /// Returns the single policy in this of type matching the filter. + /// + /// The for which to search for the policy. + /// A filter to apply to any policies of type found. + /// The type of policy to return. + /// A matching if one is found; else null. + /// InvalidOperationException, if more than one policy of the type is found in the wrap. + public static TPolicy GetPolicy(this IPolicyWrap policyWrap, Func filter) + { + if (filter == null) throw new ArgumentNullException(nameof(filter)); + return policyWrap.GetPolicies().OfType().SingleOrDefault(filter); + } + } +} diff --git a/src/Polly.Shared/Wrap/PolicyWrap.cs b/src/Polly.Shared/Wrap/PolicyWrap.cs index 1baab869f7e..4e8b113369b 100644 --- a/src/Polly.Shared/Wrap/PolicyWrap.cs +++ b/src/Polly.Shared/Wrap/PolicyWrap.cs @@ -9,8 +9,8 @@ namespace Polly.Wrap /// public partial class PolicyWrap : Policy, IPolicyWrap { - private Policy _outer; - private Policy _inner; + private IsPolicy _outer; + private IsPolicy _inner; /// /// Returns the outer in this @@ -22,7 +22,7 @@ public partial class PolicyWrap : Policy, IPolicyWrap /// public IsPolicy Inner => _inner; - internal PolicyWrap(Action, Context, CancellationToken> policyAction, Policy outer, Policy inner) + internal PolicyWrap(Action, Context, CancellationToken> policyAction, Policy outer, ISyncPolicy inner) : base(policyAction, outer.ExceptionPredicates) { _outer = outer; @@ -43,8 +43,8 @@ public override TResult ExecuteInternal(Func, Context, CancellationToken, bool, Task> policyAction, Policy outer, Policy inner) + internal PolicyWrap(Func, Context, CancellationToken, bool, Task> policyAction, Policy outer, IAsyncPolicy inner) : base(policyAction, outer.ExceptionPredicates) { _outer = outer; @@ -30,8 +30,8 @@ public override Task ExecuteAsyncInternal(Func( Func func, Context context, CancellationToken cancellationToken, - Policy outerPolicy, - Policy innerPolicy) + ISyncPolicy outerPolicy, + ISyncPolicy innerPolicy) { return outerPolicy.Execute((ctx, ct) => innerPolicy.Execute(func, ctx, ct), context, cancellationToken); } @@ -19,8 +19,8 @@ internal static TResult Implementation( Func func, Context context, CancellationToken cancellationToken, - Policy outerPolicy, - Policy innerPolicy) + ISyncPolicy outerPolicy, + ISyncPolicy innerPolicy) { return outerPolicy.Execute((ctx, ct) => innerPolicy.Execute(func, ctx, ct), context, cancellationToken); } @@ -29,8 +29,8 @@ internal static TResult Implementation( Func func, Context context, CancellationToken cancellationToken, - Policy outerPolicy, - Policy innerPolicy) + ISyncPolicy outerPolicy, + ISyncPolicy innerPolicy) { return outerPolicy.Execute((ctx, ct) => innerPolicy.Execute(func, ctx, ct), context, cancellationToken); } @@ -39,8 +39,8 @@ internal static TResult Implementation( Func func, Context context, CancellationToken cancellationToken, - Policy outerPolicy, - Policy innerPolicy) + ISyncPolicy outerPolicy, + ISyncPolicy innerPolicy) { return outerPolicy.Execute((ctx, ct) => innerPolicy.Execute(func, ctx, ct), context, cancellationToken); } @@ -49,8 +49,8 @@ internal static void Implementation( Action action, Context context, CancellationToken cancellationToken, - Policy outerPolicy, - Policy innerPolicy) + ISyncPolicy outerPolicy, + ISyncPolicy innerPolicy) { outerPolicy.Execute((ctx, ct) => innerPolicy.Execute(action, ctx, ct), context, cancellationToken); } diff --git a/src/Polly.Shared/Wrap/PolicyWrapEngineAsync.cs b/src/Polly.Shared/Wrap/PolicyWrapEngineAsync.cs index 0f162095ca3..ab0fad85e79 100644 --- a/src/Polly.Shared/Wrap/PolicyWrapEngineAsync.cs +++ b/src/Polly.Shared/Wrap/PolicyWrapEngineAsync.cs @@ -11,8 +11,8 @@ internal static async Task ImplementationAsync( Context context, CancellationToken cancellationToken, bool continueOnCapturedContext, - Policy outerPolicy, - Policy innerPolicy) + IAsyncPolicy outerPolicy, + IAsyncPolicy innerPolicy) { return await outerPolicy.ExecuteAsync( async (ctx, ct) => await innerPolicy.ExecuteAsync( @@ -32,8 +32,8 @@ internal static async Task ImplementationAsync( Context context, CancellationToken cancellationToken, bool continueOnCapturedContext, - Policy outerPolicy, - Policy innerPolicy) + IAsyncPolicy outerPolicy, + IAsyncPolicy innerPolicy) { return await outerPolicy.ExecuteAsync( async (ctx, ct) => await innerPolicy.ExecuteAsync( @@ -53,8 +53,8 @@ internal static async Task ImplementationAsync( Context context, CancellationToken cancellationToken, bool continueOnCapturedContext, - Policy outerPolicy, - Policy innerPolicy) + IAsyncPolicy outerPolicy, + IAsyncPolicy innerPolicy) { return await outerPolicy.ExecuteAsync( async (ctx, ct) => await innerPolicy.ExecuteAsync( @@ -74,8 +74,8 @@ internal static async Task ImplementationAsync( Context context, CancellationToken cancellationToken, bool continueOnCapturedContext, - Policy outerPolicy, - Policy innerPolicy) + IAsyncPolicy outerPolicy, + IAsyncPolicy innerPolicy) { return await outerPolicy.ExecuteAsync( async (ctx, ct) => await innerPolicy.ExecuteAsync( @@ -95,8 +95,8 @@ internal static async Task ImplementationAsync( Context context, CancellationToken cancellationToken, bool continueOnCapturedContext, - Policy outerPolicy, - Policy innerPolicy) + IAsyncPolicy outerPolicy, + IAsyncPolicy innerPolicy) { await outerPolicy.ExecuteAsync( async (ctx, ct) => await innerPolicy.ExecuteAsync( diff --git a/src/Polly.Shared/Wrap/PolicyWrapSyntax.cs b/src/Polly.Shared/Wrap/PolicyWrapSyntax.cs index acfb6969eb2..59c85ec6cab 100644 --- a/src/Polly.Shared/Wrap/PolicyWrapSyntax.cs +++ b/src/Polly.Shared/Wrap/PolicyWrapSyntax.cs @@ -12,7 +12,7 @@ public partial class Policy /// /// The inner policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap Wrap(Policy innerPolicy) + public PolicyWrap Wrap(ISyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -29,7 +29,7 @@ public PolicyWrap Wrap(Policy innerPolicy) /// The inner policy. /// The return type of delegates which may be executed through the policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap Wrap(Policy innerPolicy) + public PolicyWrap Wrap(ISyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -48,7 +48,7 @@ public partial class Policy /// /// The inner policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap Wrap(Policy innerPolicy) + public PolicyWrap Wrap(ISyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -64,7 +64,7 @@ public PolicyWrap Wrap(Policy innerPolicy) /// /// The inner policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap Wrap(Policy innerPolicy) + public PolicyWrap Wrap(ISyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -84,16 +84,25 @@ public partial class Policy /// The policies to place in the wrap, outermost (at left) to innermost (at right). /// The PolicyWrap. /// The enumerable of policies to form the wrap must contain at least two policies. - public static PolicyWrap Wrap(params Policy[] policies) + public static PolicyWrap Wrap(params ISyncPolicy[] policies) { - switch (policies.Count()) + switch (policies.Length) { case 0: case 1: throw new ArgumentException("The enumerable of policies to form the wrap must contain at least two policies.", nameof(policies)); + case 2: + return new PolicyWrap( + (func, context, cancellationtoken) => PolicyWrapEngine.Implementation( + func, + context, + cancellationtoken, + policies[0], + policies[1]), + (Policy)policies[0], policies[1]); + default: - IEnumerable remainder = policies.Skip(1); - return policies.First().Wrap(remainder.Count() == 1 ? remainder.Single() : Wrap(remainder.ToArray())); + return Wrap(policies[0], Wrap(policies.Skip(1).ToArray())); } } @@ -104,16 +113,25 @@ public static PolicyWrap Wrap(params Policy[] policies) /// The return type of delegates which may be executed through the policy. /// The PolicyWrap. /// The enumerable of policies to form the wrap must contain at least two policies. - public static PolicyWrap Wrap(params Policy[] policies) + public static PolicyWrap Wrap(params ISyncPolicy[] policies) { - switch (policies.Count()) + switch (policies.Length) { case 0: case 1: throw new ArgumentException("The enumerable of policies to form the wrap must contain at least two policies.", nameof(policies)); + case 2: + return new PolicyWrap( + (func, context, cancellationtoken) => PolicyWrapEngine.Implementation( + func, + context, + cancellationtoken, + policies[0], + policies[1]), + (Policy)policies[0], policies[1]); + default: - IEnumerable> remainder = policies.Skip(1); - return policies.First().Wrap(remainder.Count() == 1 ? remainder.Single() : Wrap(remainder.ToArray())); + return Wrap(policies[0], Wrap(policies.Skip(1).ToArray())); } } } diff --git a/src/Polly.Shared/Wrap/PolicyWrapSyntaxAsync.cs b/src/Polly.Shared/Wrap/PolicyWrapSyntaxAsync.cs index 8f1cfc32476..2c9b8507b20 100644 --- a/src/Polly.Shared/Wrap/PolicyWrapSyntaxAsync.cs +++ b/src/Polly.Shared/Wrap/PolicyWrapSyntaxAsync.cs @@ -12,7 +12,7 @@ public partial class Policy /// /// The inner policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap WrapAsync(Policy innerPolicy) + public PolicyWrap WrapAsync(IAsyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -29,7 +29,7 @@ public PolicyWrap WrapAsync(Policy innerPolicy) /// The inner policy. /// The return type of delegates which may be executed through the policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap WrapAsync(Policy innerPolicy) + public PolicyWrap WrapAsync(IAsyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -48,7 +48,7 @@ public partial class Policy /// /// The inner policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap WrapAsync(Policy innerPolicy) + public PolicyWrap WrapAsync(IAsyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -64,7 +64,7 @@ public PolicyWrap WrapAsync(Policy innerPolicy) /// /// The inner policy. /// PolicyWrap.PolicyWrap. - public PolicyWrap WrapAsync(Policy innerPolicy) + public PolicyWrap WrapAsync(IAsyncPolicy innerPolicy) { if (innerPolicy == null) throw new ArgumentNullException(nameof(innerPolicy)); @@ -84,16 +84,26 @@ public partial class Policy /// The policies to place in the wrap, outermost (at left) to innermost (at right). /// The PolicyWrap. /// The enumerable of policies to form the wrap must contain at least two policies. - public static PolicyWrap WrapAsync(params Policy[] policies) + public static PolicyWrap WrapAsync(params IAsyncPolicy[] policies) { - switch (policies.Count()) + switch (policies.Length) { case 0: case 1: throw new ArgumentException("The enumerable of policies to form the wrap must contain at least two policies.", nameof(policies)); + case 2: + return new PolicyWrap( + (func, context, cancellationtoken, continueOnCapturedContext) => PolicyWrapEngine.ImplementationAsync( + func, + context, + cancellationtoken, + continueOnCapturedContext, + policies[0], + policies[1]), + (Policy)policies[0], policies[1]); + default: - IEnumerable remainder = policies.Skip(1); - return policies.First().WrapAsync(remainder.Count() == 1 ? remainder.Single() : WrapAsync(remainder.ToArray())); + return WrapAsync(policies[0], WrapAsync(policies.Skip(1).ToArray())); } } @@ -104,16 +114,26 @@ public static PolicyWrap WrapAsync(params Policy[] policies) /// The return type of delegates which may be executed through the policy. /// The PolicyWrap. /// The enumerable of policies to form the wrap must contain at least two policies. - public static PolicyWrap WrapAsync(params Policy[] policies) + public static PolicyWrap WrapAsync(params IAsyncPolicy[] policies) { - switch (policies.Count()) + switch (policies.Length) { case 0: case 1: throw new ArgumentException("The enumerable of policies to form the wrap must contain at least two policies.", nameof(policies)); + case 2: + return new PolicyWrap( + (func, context, cancellationtoken, continueOnCapturedContext) => PolicyWrapEngine.ImplementationAsync( + func, + context, + cancellationtoken, + continueOnCapturedContext, + policies[0], + policies[1]), + (Policy)policies[0], policies[1]); + default: - IEnumerable> remainder = policies.Skip(1); - return policies.First().WrapAsync(remainder.Count() == 1 ? remainder.Single() : WrapAsync(remainder.ToArray())); + return WrapAsync(policies[0], WrapAsync(policies.Skip(1).ToArray())); } } } diff --git a/src/Polly.SharedSpecs/Caching/AbsoluteTtlSpecs.cs b/src/Polly.SharedSpecs/Caching/AbsoluteTtlSpecs.cs index e81d90573b5..524ed135e0f 100644 --- a/src/Polly.SharedSpecs/Caching/AbsoluteTtlSpecs.cs +++ b/src/Polly.SharedSpecs/Caching/AbsoluteTtlSpecs.cs @@ -6,6 +6,7 @@ namespace Polly.Specs.Caching { + [Collection("SystemClockDependantCollection")] public class AbsoluteTtlSpecs : IDisposable { [Fact] diff --git a/src/Polly.SharedSpecs/Caching/CacheAsyncSpecs.cs b/src/Polly.SharedSpecs/Caching/CacheAsyncSpecs.cs index 50c03866443..ddc411cd64c 100644 --- a/src/Polly.SharedSpecs/Caching/CacheAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Caching/CacheAsyncSpecs.cs @@ -11,6 +11,7 @@ namespace Polly.Specs.Caching { + [Collection("SystemClockDependantCollection")] public class CacheAsyncSpecs : IDisposable { #region Configuration diff --git a/src/Polly.SharedSpecs/Caching/CacheSpecs.cs b/src/Polly.SharedSpecs/Caching/CacheSpecs.cs index c9c860de4d2..9f991c17f8c 100644 --- a/src/Polly.SharedSpecs/Caching/CacheSpecs.cs +++ b/src/Polly.SharedSpecs/Caching/CacheSpecs.cs @@ -10,6 +10,7 @@ namespace Polly.Specs.Caching { + [Collection("SystemClockDependantCollection")] public class CacheSpecs : IDisposable { #region Configuration diff --git a/src/Polly.SharedSpecs/Caching/CacheTResultAsyncSpecs.cs b/src/Polly.SharedSpecs/Caching/CacheTResultAsyncSpecs.cs index 9c1f4cf09c4..cc607e8d925 100644 --- a/src/Polly.SharedSpecs/Caching/CacheTResultAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Caching/CacheTResultAsyncSpecs.cs @@ -11,6 +11,7 @@ namespace Polly.Specs.Caching { + [Collection("SystemClockDependantCollection")] public class CacheTResultAsyncSpecs : IDisposable { #region Configuration diff --git a/src/Polly.SharedSpecs/Caching/CacheTResultSpecs.cs b/src/Polly.SharedSpecs/Caching/CacheTResultSpecs.cs index 52d1d7a8cd1..b497d93f34f 100644 --- a/src/Polly.SharedSpecs/Caching/CacheTResultSpecs.cs +++ b/src/Polly.SharedSpecs/Caching/CacheTResultSpecs.cs @@ -10,6 +10,7 @@ namespace Polly.Specs.Caching { + [Collection("SystemClockDependantCollection")] public class CacheTResultSpecs : IDisposable { #region Configuration diff --git a/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs b/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs index 7b654e24b6c..6442357b3cf 100644 --- a/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerAsyncSpecs.cs @@ -12,6 +12,7 @@ namespace Polly.Specs.CircuitBreaker { + [Collection("SystemClockDependantCollection")] public class AdvancedCircuitBreakerAsyncSpecs : IDisposable { #region Configuration tests diff --git a/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs b/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs index b3c6a0640be..a37b97897f6 100644 --- a/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs +++ b/src/Polly.SharedSpecs/CircuitBreaker/AdvancedCircuitBreakerSpecs.cs @@ -12,6 +12,8 @@ namespace Polly.Specs.CircuitBreaker { + + [Collection("SystemClockDependantCollection")] public class AdvancedCircuitBreakerSpecs : IDisposable { #region Configuration tests diff --git a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerAsyncSpecs.cs b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerAsyncSpecs.cs index 938a2630936..8f334117f2d 100644 --- a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerAsyncSpecs.cs @@ -12,6 +12,7 @@ namespace Polly.Specs.CircuitBreaker { + [Collection("SystemClockDependantCollection")] public class CircuitBreakerAsyncSpecs : IDisposable { #region Configuration tests @@ -1049,6 +1050,35 @@ public void Should_call_onbreak_with_the_last_raised_exception() passedException?.Should().BeOfType(); } + [Fact] + public void Should_rethrow_and_call_onbreak_with_the_last_raised_exception_unwrapped_if_matched_as_inner() + { + Exception passedException = null; + + Action onBreak = (exception, _, __) => { passedException = exception; }; + Action onReset = _ => { }; + + TimeSpan durationOfBreak = TimeSpan.FromMinutes(1); + + CircuitBreakerPolicy breaker = Policy + .HandleInner() + .Or() + .CircuitBreakerAsync(2, durationOfBreak, onBreak, onReset); + + Exception toRaiseAsInner = new DivideByZeroException(); + Exception withInner = new AggregateException(toRaiseAsInner); + + breaker.Awaiting(x => x.RaiseExceptionAsync()) + .ShouldThrow(); + + breaker.Awaiting(x => x.RaiseExceptionAsync(withInner)) + .ShouldThrow().Which.Should().BeSameAs(toRaiseAsInner); + + breaker.CircuitState.Should().Be(CircuitState.Open); + + passedException?.Should().BeSameAs(toRaiseAsInner); + } + [Fact] public void Should_call_onbreak_with_the_correct_timespan() { @@ -1252,6 +1282,24 @@ public void Should_set_LastException_on_handling_exception_even_when_not_breakin breaker.LastException.Should().BeOfType(); } + [Fact] + public void Should_set_LastException_on_handling_inner_exception_even_when_not_breaking() + { + CircuitBreakerPolicy breaker = Policy + .HandleInner() + .CircuitBreakerAsync(2, TimeSpan.FromMinutes(1)); + + Exception toRaiseAsInner = new DivideByZeroException(); + Exception withInner = new AggregateException(toRaiseAsInner); + + breaker.Awaiting(x => x.RaiseExceptionAsync(withInner)) + .ShouldThrow().Which.Should().BeSameAs(toRaiseAsInner); + + breaker.CircuitState.Should().Be(CircuitState.Closed); + + breaker.LastException.Should().BeSameAs(toRaiseAsInner); + } + [Fact] public void Should_set_LastException_to_last_raised_exception_when_breaking() { diff --git a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerSpecs.cs b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerSpecs.cs index eb2e35110fc..1108e83ca96 100644 --- a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerSpecs.cs +++ b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerSpecs.cs @@ -10,6 +10,7 @@ namespace Polly.Specs.CircuitBreaker { + [Collection("SystemClockDependantCollection")] public class CircuitBreakerSpecs : IDisposable { #region Configuration tests @@ -1041,6 +1042,35 @@ public void Should_call_onbreak_with_the_last_raised_exception() passedException?.Should().BeOfType(); } + [Fact] + public void Should_rethrow_and_call_onbreak_with_the_last_raised_exception_unwrapped_if_matched_as_inner() + { + Exception passedException = null; + + Action onBreak = (exception, _, __) => { passedException = exception; }; + Action onReset = _ => { }; + + TimeSpan durationOfBreak = TimeSpan.FromMinutes(1); + + CircuitBreakerPolicy breaker = Policy + .HandleInner() + .Or() + .CircuitBreaker(2, durationOfBreak, onBreak, onReset); + + Exception toRaiseAsInner = new DivideByZeroException(); + Exception withInner = new AggregateException(toRaiseAsInner); + + breaker.Invoking(x => x.RaiseException()) + .ShouldThrow(); + + breaker.Invoking(x => x.RaiseException(withInner)) + .ShouldThrow().Which.Should().BeSameAs(toRaiseAsInner); + + breaker.CircuitState.Should().Be(CircuitState.Open); + + passedException?.Should().BeSameAs(toRaiseAsInner); + } + [Fact] public void Should_call_onbreak_with_the_correct_timespan() { @@ -1246,6 +1276,25 @@ public void Should_set_LastException_on_handling_exception_even_when_not_breakin breaker.LastException.Should().BeOfType(); } + [Fact] + public void Should_set_LastException_on_handling_inner_exception_even_when_not_breaking() + { + CircuitBreakerPolicy breaker = Policy + .HandleInner() + .CircuitBreaker(2, TimeSpan.FromMinutes(1)); + + + Exception toRaiseAsInner = new DivideByZeroException(); + Exception withInner = new AggregateException(toRaiseAsInner); + + breaker.Invoking(x => x.RaiseException(withInner)) + .ShouldThrow().Which.Should().BeSameAs(toRaiseAsInner); + + breaker.CircuitState.Should().Be(CircuitState.Closed); + + breaker.LastException.Should().BeSameAs(toRaiseAsInner); + } + [Fact] public void Should_set_LastException_to_last_raised_exception_when_breaking() { @@ -1288,6 +1337,27 @@ public void Should_set_LastException_to_null_on_circuit_reset() #endregion + #region ExecuteAndCapture with HandleInner + + [Fact] + public void Should_set_PolicyResult_on_handling_inner_exception() + { + CircuitBreakerPolicy breaker = Policy + .HandleInner() + .CircuitBreaker(2, TimeSpan.FromMinutes(1)); + + + Exception toRaiseAsInner = new DivideByZeroException(); + Exception withInner = new AggregateException(toRaiseAsInner); + + PolicyResult policyResult = breaker.ExecuteAndCapture(() => throw withInner); + + policyResult.ExceptionType.Should().Be(ExceptionType.HandledByThisPolicy); + policyResult.FinalException.Should().BeSameAs(toRaiseAsInner); + } + + #endregion + #region Cancellation support [Fact] diff --git a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultAsyncSpecs.cs b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultAsyncSpecs.cs index b1413e51194..e103b075689 100644 --- a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultAsyncSpecs.cs @@ -13,6 +13,7 @@ namespace Polly.Specs.CircuitBreaker { + [Collection("SystemClockDependantCollection")] public class CircuitBreakerTResultAsyncSpecs : IDisposable { #region Configuration tests diff --git a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultMixedResultExceptionSpecs.cs b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultMixedResultExceptionSpecs.cs index a20624aa6de..2a2a0a90b1a 100644 --- a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultMixedResultExceptionSpecs.cs +++ b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultMixedResultExceptionSpecs.cs @@ -8,6 +8,7 @@ namespace Polly.Specs.CircuitBreaker { + [Collection("SystemClockDependantCollection")] public class CircuitBreakerTResultMixedResultExceptionSpecs : IDisposable { #region Circuit-breaker threshold-to-break tests diff --git a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultSpecs.cs b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultSpecs.cs index 1e1a59e5cd1..46bd07544a2 100644 --- a/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultSpecs.cs +++ b/src/Polly.SharedSpecs/CircuitBreaker/CircuitBreakerTResultSpecs.cs @@ -12,6 +12,7 @@ namespace Polly.Specs.CircuitBreaker { + [Collection("SystemClockDependantCollection")] public class CircuitBreakerTResultSpecs : IDisposable { #region Configuration tests diff --git a/src/Polly.SharedSpecs/Fallback/FallbackAsyncSpecs.cs b/src/Polly.SharedSpecs/Fallback/FallbackAsyncSpecs.cs index 1cb21e067b7..e98775ec5d8 100644 --- a/src/Polly.SharedSpecs/Fallback/FallbackAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Fallback/FallbackAsyncSpecs.cs @@ -90,7 +90,7 @@ public void Should_throw_when_onFallback_delegate_is_null_with_context() #region Policy operation tests [Fact] - public async Task Should_not_execute_fallback_when_execute_delegate_does_not_throw() + public async Task Should_not_execute_fallback_when_executed_delegate_does_not_throw() { bool fallbackActionExecuted = false; Func fallbackActionAsync = _ => { fallbackActionExecuted = true; return TaskHelper.EmptyTask; }; @@ -105,7 +105,7 @@ public async Task Should_not_execute_fallback_when_execute_delegate_does_not_thr } [Fact] - public void Should_not_execute_fallback_when_execute_delegate_throws_exception_not_handled_by_policy() + public void Should_not_execute_fallback_when_executed_delegate_throws_exception_not_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackActionAsync = _ => { fallbackActionExecuted = true; return TaskHelper.EmptyTask; }; @@ -120,7 +120,7 @@ public void Should_not_execute_fallback_when_execute_delegate_throws_exception_n } [Fact] - public void Should_execute_fallback_when_execute_delegate_throws_exception_handled_by_policy() + public void Should_execute_fallback_when_executed_delegate_throws_exception_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackActionAsync = _ => { fallbackActionExecuted = true; return TaskHelper.EmptyTask; }; @@ -136,7 +136,7 @@ public void Should_execute_fallback_when_execute_delegate_throws_exception_handl [Fact] - public void Should_execute_fallback_when_execute_delegate_throws_one_of_exceptions_handled_by_policy() + public void Should_execute_fallback_when_executed_delegate_throws_one_of_exceptions_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackActionAsync = _ => { fallbackActionExecuted = true; return TaskHelper.EmptyTask; }; @@ -153,7 +153,7 @@ public void Should_execute_fallback_when_execute_delegate_throws_one_of_exceptio [Fact] - public void Should_not_execute_fallback_when_execute_delegate_throws_exception_not_one_of_exceptions_handled_by_policy() + public void Should_not_execute_fallback_when_executed_delegate_throws_exception_not_one_of_exceptions_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackActionAsync = _ => { fallbackActionExecuted = true; return TaskHelper.EmptyTask; }; @@ -251,6 +251,16 @@ public void Should_not_handle_exception_thrown_by_fallback_delegate_even_if_is_e fallbackActionExecuted.Should().BeTrue(); } + [Fact] + public void Should_throw_for_generic_method_execution_on_non_generic_policy() + { + FallbackPolicy fallbackPolicy = Policy + .Handle() + .FallbackAsync(_ => TaskHelper.EmptyTask); + + fallbackPolicy.Awaiting(p => p.ExecuteAsync(() => TaskHelper.FromResult(0))).ShouldThrow(); + } + #endregion #region onPolicyEvent delegate tests @@ -277,7 +287,7 @@ public async Task Should_call_onFallback_passing_exception_triggering_fallback() } [Fact] - public async Task Should_not_call_onFallback_when_execute_delegate_does_not_throw() + public async Task Should_not_call_onFallback_when_executed_delegate_does_not_throw() { Func fallbackActionAsync = _ => TaskHelper.EmptyTask; @@ -500,6 +510,47 @@ public void Should_call_fallbackAction_with_the_exception_when_execute_and_captu .And.BeOfType(typeof(ArgumentNullException)); } + public void Should_call_fallbackAction_with_the_matched_inner_exception_unwrapped() + { + Exception fallbackException = null; + + Func fallbackFunc = (ex, ctx, ct) => { fallbackException = ex; return TaskHelper.EmptyTask; }; + + Func onFallback = (ex, ctx) => { return TaskHelper.EmptyTask; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .FallbackAsync(fallbackFunc, onFallback); + + Exception instanceToCapture = new ArgumentNullException("myParam"); + Exception instanceToThrow = new Exception(String.Empty, instanceToCapture); + fallbackPolicy.Awaiting(p => p.RaiseExceptionAsync(instanceToThrow)) + .ShouldNotThrow(); + + fallbackException.Should().Be(instanceToCapture); + } + + [Fact] + public void Should_call_fallbackAction_with_the_matched_inner_of_aggregate_exception_unwrapped() + { + Exception fallbackException = null; + + Func fallbackFunc = (ex, ctx, ct) => { fallbackException = ex; return TaskHelper.EmptyTask; }; + + Func onFallback = (ex, ctx) => { return TaskHelper.EmptyTask; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .FallbackAsync(fallbackFunc, onFallback); + + Exception instanceToCapture = new ArgumentNullException("myParam"); + Exception instanceToThrow = new AggregateException(instanceToCapture); + fallbackPolicy.Awaiting(p => p.RaiseExceptionAsync(instanceToThrow)) + .ShouldNotThrow(); + + fallbackException.Should().Be(instanceToCapture); + } + [Fact] public void Should_not_call_fallbackAction_with_the_exception_if_exception_unhandled() { diff --git a/src/Polly.SharedSpecs/Fallback/FallbackSpecs.cs b/src/Polly.SharedSpecs/Fallback/FallbackSpecs.cs index 36de4307890..d3c1e3172ef 100644 --- a/src/Polly.SharedSpecs/Fallback/FallbackSpecs.cs +++ b/src/Polly.SharedSpecs/Fallback/FallbackSpecs.cs @@ -157,7 +157,7 @@ public void Should_throw_when_onFallback_delegate_is_null_with_context_with_acti #region Policy operation tests [Fact] - public void Should_not_execute_fallback_when_execute_delegate_does_not_throw() + public void Should_not_execute_fallback_when_executed_delegate_does_not_throw() { bool fallbackActionExecuted = false; Action fallbackAction = () => { fallbackActionExecuted = true; }; @@ -172,7 +172,7 @@ public void Should_not_execute_fallback_when_execute_delegate_does_not_throw() } [Fact] - public void Should_not_execute_fallback_when_execute_delegate_throws_exception_not_handled_by_policy() + public void Should_not_execute_fallback_when_executed_delegate_throws_exception_not_handled_by_policy() { bool fallbackActionExecuted = false; Action fallbackAction = () => { fallbackActionExecuted = true; }; @@ -186,41 +186,40 @@ public void Should_not_execute_fallback_when_execute_delegate_throws_exception_n fallbackActionExecuted.Should().BeFalse(); } + [Fact] - public void Should_execute_fallback_when_execute_delegate_throws_exception_handled_by_policy() + public void Should_execute_fallback_when_executed_delegate_throws_exception_handled_by_policy() { bool fallbackActionExecuted = false; Action fallbackAction = () => { fallbackActionExecuted = true; }; FallbackPolicy fallbackPolicy = Policy - .Handle() - .Fallback(fallbackAction); + .Handle() + .Fallback(fallbackAction); fallbackPolicy.Invoking(x => x.RaiseException()).ShouldNotThrow(); fallbackActionExecuted.Should().BeTrue(); } - [Fact] - public void Should_execute_fallback_when_execute_delegate_throws_one_of_exceptions_handled_by_policy() + public void Should_execute_fallback_when_executed_delegate_throws_one_of_exceptions_handled_by_policy() { bool fallbackActionExecuted = false; Action fallbackAction = () => { fallbackActionExecuted = true; }; FallbackPolicy fallbackPolicy = Policy - .Handle() - .Or() - .Fallback(fallbackAction); + .Handle() + .Or() + .Fallback(fallbackAction); fallbackPolicy.Invoking(x => x.RaiseException()).ShouldNotThrow(); fallbackActionExecuted.Should().BeTrue(); } - [Fact] - public void Should_not_execute_fallback_when_execute_delegate_throws_exception_not_one_of_exceptions_handled_by_policy() + public void Should_not_execute_fallback_when_executed_delegate_throws_exception_not_one_of_exceptions_handled_by_policy() { bool fallbackActionExecuted = false; Action fallbackAction = () => { fallbackActionExecuted = true; }; @@ -273,15 +272,14 @@ public void Should_execute_fallback_when_exception_thrown_matches_handling_predi Action fallbackAction = () => { fallbackActionExecuted = true; }; FallbackPolicy fallbackPolicy = Policy - .Handle(e => true) - .Fallback(fallbackAction); + .Handle(e => true) + .Fallback(fallbackAction); fallbackPolicy.Invoking(x => x.RaiseException()).ShouldNotThrow(); fallbackActionExecuted.Should().BeTrue(); } - [Fact] public void Should_execute_fallback_when_exception_thrown_matches_one_of_handling_predicates() { @@ -289,9 +287,9 @@ public void Should_execute_fallback_when_exception_thrown_matches_one_of_handlin Action fallbackAction = () => { fallbackActionExecuted = true; }; FallbackPolicy fallbackPolicy = Policy - .Handle(e => true) - .Or() - .Fallback(fallbackAction); + .Handle(e => true) + .Or() + .Fallback(fallbackAction); fallbackPolicy.Invoking(x => x.RaiseException()).ShouldNotThrow(); @@ -318,6 +316,408 @@ public void Should_not_handle_exception_thrown_by_fallback_delegate_even_if_is_e fallbackActionExecuted.Should().BeTrue(); } + [Fact] + public void Should_throw_for_generic_method_execution_on_non_generic_policy() + { + FallbackPolicy fallbackPolicy = Policy + .Handle() + .Fallback(() => {}); + + fallbackPolicy.Invoking(p => p.Execute(() => 0)).ShouldThrow(); + } + + #endregion + + #region HandleInner tests, inner of normal exceptions + + [Fact] + public void Should_not_execute_fallback_when_executed_delegate_throws_inner_exception_where_policy_doesnt_handle_inner() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .Handle() + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerException.Should().BeOfType(); + + fallbackActionExecuted.Should().BeFalse(); + } + + [Fact] + public void Should_execute_fallback_when_policy_handles_inner_and_executed_delegate_throws_as_non_inner() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception nonInner = new DivideByZeroException(); + + fallbackPolicy.Invoking(x => x.RaiseException(nonInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_executed_delegate_throws_inner_exception_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_executed_delegate_throws_one_of_inner_exceptions_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .Handle() + .OrInner() + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new ArgumentException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_executed_delegate_throws_nested_inner_exception_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new Exception(String.Empty, new Exception(String.Empty, new DivideByZeroException()))); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + + [Fact] + public void Should_not_execute_fallback_when_executed_delegate_throws_inner_exception_not_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new ArgumentException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerException.Should().BeOfType(); + + fallbackActionExecuted.Should().BeFalse(); + } + + [Fact] + public void Should_execute_fallback_when_inner_exception_thrown_matches_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => true) + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_inner_nested_exception_thrown_matches_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => true) + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new Exception(String.Empty, new Exception(String.Empty, new DivideByZeroException()))); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + + [Fact] + public void Should_execute_fallback_when_inner_exception_thrown_matches_one_of_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => true) + .OrInner(e => true) + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new ArgumentNullException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_not_execute_fallback_when_inner_exception_thrown_does_not_match_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => false) + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerException.Should().BeOfType(); + + fallbackActionExecuted.Should().BeFalse(); + } + + [Fact] + public void Should_not_execute_fallback_when_inner_exception_thrown_does_not_match_any_of_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => false) + .OrInner(e => false) + .Fallback(fallbackAction); + + Exception withInner = new Exception(String.Empty, new ArgumentNullException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerException.Should().BeOfType(); + + fallbackActionExecuted.Should().BeFalse(); + } + + #endregion + + + #region HandleInner tests, inner of aggregate exceptions + + [Fact] + public void Should_not_execute_fallback_when_executed_delegate_throws_inner_of_aggregate_exception_where_policy_doesnt_handle_inner() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .Handle() + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerExceptions.Should().ContainSingle(e => e is DivideByZeroException); + + fallbackActionExecuted.Should().BeFalse(); + } + + [Fact] + public void Should_execute_fallback_when_executed_delegate_throws_inner_of_aggregate_exception_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_executed_delegate_throws_one_of_inner_of_aggregate_exceptions_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .Handle() + .OrInner() + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new ArgumentException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_executed_delegate_throws_aggregate_exception_with_inner_handled_by_policy_amongst_other_inners() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new ArgumentException(), new DivideByZeroException(), new ArgumentNullException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_executed_delegate_throws_nested_inner_of_aggregate_exception_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new AggregateException(new DivideByZeroException())); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_not_execute_fallback_when_executed_delegate_throws_inner_of_aggregate_exception_not_handled_by_policy() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new ArgumentException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerExceptions.Should().ContainSingle(e => e is ArgumentException); + + fallbackActionExecuted.Should().BeFalse(); + } + + [Fact] + public void Should_execute_fallback_when_inner_of_aggregate_exception_thrown_matches_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => true) + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_execute_fallback_when_inner_of_aggregate_nested_exception_thrown_matches_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => true) + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new AggregateException(new DivideByZeroException())); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + + [Fact] + public void Should_execute_fallback_when_inner_of_aggregate_exception_thrown_matches_one_of_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => true) + .OrInner(e => true) + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new ArgumentNullException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldNotThrow(); + + fallbackActionExecuted.Should().BeTrue(); + } + + [Fact] + public void Should_not_execute_fallback_when_inner_of_aggregate_exception_thrown_does_not_match_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => false) + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new DivideByZeroException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerExceptions.Should().ContainSingle(e => e is DivideByZeroException); + + fallbackActionExecuted.Should().BeFalse(); + } + + [Fact] + public void Should_not_execute_fallback_when_inner_of_aggregate_exception_thrown_does_not_match_any_of_handling_predicates() + { + bool fallbackActionExecuted = false; + Action fallbackAction = () => { fallbackActionExecuted = true; }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner(e => false) + .OrInner(e => false) + .Fallback(fallbackAction); + + Exception withInner = new AggregateException(new ArgumentNullException()); + + fallbackPolicy.Invoking(x => x.RaiseException(withInner)).ShouldThrow().And.InnerExceptions.Should().ContainSingle(e => e is ArgumentNullException); + + fallbackActionExecuted.Should().BeFalse(); + } + #endregion #region onPolicyEvent delegate tests @@ -344,7 +744,7 @@ public void Should_call_onFallback_passing_exception_triggering_fallback() } [Fact] - public void Should_not_call_onFallback_when_execute_delegate_does_not_throw() + public void Should_not_call_onFallback_when_executed_delegate_does_not_throw() { Action fallbackAction = () => { }; @@ -566,6 +966,48 @@ public void Should_call_fallbackAction_with_the_exception_when_execute_and_captu .And.BeOfType(typeof(ArgumentNullException)); } + [Fact] + public void Should_call_fallbackAction_with_the_matched_inner_exception_unwrapped() + { + Exception fallbackException = null; + + Action fallbackAction = (ex, ctx, ct) => { fallbackException = ex; }; + + Action onFallback = (ex, ctx) => { }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction, onFallback); + + Exception instanceToCapture = new ArgumentNullException("myParam"); + Exception instanceToThrow = new Exception(String.Empty, instanceToCapture); + fallbackPolicy.Invoking(p => p.RaiseException(instanceToThrow)) + .ShouldNotThrow(); + + fallbackException.Should().Be(instanceToCapture); + } + + [Fact] + public void Should_call_fallbackAction_with_the_matched_inner_of_aggregate_exception_unwrapped() + { + Exception fallbackException = null; + + Action fallbackAction = (ex, ctx, ct) => { fallbackException = ex; }; + + Action onFallback = (ex, ctx) => { }; + + FallbackPolicy fallbackPolicy = Policy + .HandleInner() + .Fallback(fallbackAction, onFallback); + + Exception instanceToCapture = new ArgumentNullException("myParam"); + Exception instanceToThrow = new AggregateException(instanceToCapture); + fallbackPolicy.Invoking(p => p.RaiseException(instanceToThrow)) + .ShouldNotThrow(); + + fallbackException.Should().Be(instanceToCapture); + } + [Fact] public void Should_not_call_fallbackAction_with_the_exception_if_exception_unhandled() { diff --git a/src/Polly.SharedSpecs/Fallback/FallbackTResultAsyncSpecs.cs b/src/Polly.SharedSpecs/Fallback/FallbackTResultAsyncSpecs.cs index b61f4a87e97..26e901486c1 100644 --- a/src/Polly.SharedSpecs/Fallback/FallbackTResultAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Fallback/FallbackTResultAsyncSpecs.cs @@ -118,7 +118,7 @@ public void Should_throw_when_onFallback_delegate_is_null_with_context_with_acti #region Policy operation tests [Fact] - public async Task Should_not_execute_fallback_when_execute_delegate_does_not_raise_fault() + public async Task Should_not_execute_fallback_when_executed_delegate_does_not_raise_fault() { bool fallbackActionExecuted = false; Func> fallbackAction = ct => { fallbackActionExecuted = true; return Task.FromResult(ResultPrimitive.Substitute); }; @@ -133,7 +133,7 @@ public async Task Should_not_execute_fallback_when_execute_delegate_does_not_rai } [Fact] - public async Task Should_not_execute_fallback_when_execute_delegate_raises_fault_not_handled_by_policy() + public async Task Should_not_execute_fallback_when_executed_delegate_raises_fault_not_handled_by_policy() { bool fallbackActionExecuted = false; Func> fallbackAction = ct => { fallbackActionExecuted = true; return Task.FromResult(ResultPrimitive.Substitute); }; @@ -149,7 +149,7 @@ public async Task Should_not_execute_fallback_when_execute_delegate_raises_fault } [Fact] - public async Task Should_return_fallback_value_when_execute_delegate_raises_fault_handled_by_policy() + public async Task Should_return_fallback_value_when_executed_delegate_raises_fault_handled_by_policy() { FallbackPolicy fallbackPolicy = Policy .HandleResult(ResultPrimitive.Fault) @@ -160,7 +160,7 @@ public async Task Should_return_fallback_value_when_execute_delegate_raises_faul } [Fact] - public async Task Should_execute_fallback_when_execute_delegate_raises_fault_handled_by_policy() + public async Task Should_execute_fallback_when_executed_delegate_raises_fault_handled_by_policy() { bool fallbackActionExecuted = false; Func> fallbackAction = ct => { fallbackActionExecuted = true; return Task.FromResult(ResultPrimitive.Substitute); }; @@ -176,7 +176,7 @@ public async Task Should_execute_fallback_when_execute_delegate_raises_fault_han } [Fact] - public async Task Should_execute_fallback_when_execute_delegate_raises_one_of_results_handled_by_policy() + public async Task Should_execute_fallback_when_executed_delegate_raises_one_of_results_handled_by_policy() { bool fallbackActionExecuted = false; Func> fallbackAction = ct => { fallbackActionExecuted = true; return Task.FromResult(ResultPrimitive.Substitute); }; @@ -193,7 +193,7 @@ public async Task Should_execute_fallback_when_execute_delegate_raises_one_of_re } [Fact] - public async Task Should_not_execute_fallback_when_execute_delegate_raises_fault_not_one_of_faults_handled_by_policy() + public async Task Should_not_execute_fallback_when_executed_delegate_raises_fault_not_one_of_faults_handled_by_policy() { bool fallbackActionExecuted = false; Func> fallbackAction = ct => { fallbackActionExecuted = true; return Task.FromResult(ResultPrimitive.Substitute); }; @@ -226,7 +226,7 @@ public async Task Should_not_execute_fallback_when_result_raised_does_not_match_ } [Fact] - public async Task Should_not_execute_fallback_when_execute_delegate_raises_fault_not_handled_by_any_of_predicates() + public async Task Should_not_execute_fallback_when_executed_delegate_raises_fault_not_handled_by_any_of_predicates() { bool fallbackActionExecuted = false; Func> fallbackAction = ct => { fallbackActionExecuted = true; return Task.FromResult(ResultPrimitive.Substitute); }; @@ -322,7 +322,7 @@ public async Task Should_call_onFallback_passing_result_triggering_fallback() } [Fact] - public async Task Should_not_call_onFallback_when_execute_delegate_does_not_raise_fault() + public async Task Should_not_call_onFallback_when_executed_delegate_does_not_raise_fault() { Func> fallbackAction = ct => Task.FromResult(ResultPrimitive.Substitute); diff --git a/src/Polly.SharedSpecs/Fallback/FallbackTResultSpecs.cs b/src/Polly.SharedSpecs/Fallback/FallbackTResultSpecs.cs index 470473dea30..d46fec8699b 100644 --- a/src/Polly.SharedSpecs/Fallback/FallbackTResultSpecs.cs +++ b/src/Polly.SharedSpecs/Fallback/FallbackTResultSpecs.cs @@ -157,7 +157,7 @@ public void Should_throw_when_onFallback_delegate_is_null_with_context_with_acti #region Policy operation tests [Fact] - public void Should_not_execute_fallback_when_execute_delegate_does_not_raise_fault() + public void Should_not_execute_fallback_when_executed_delegate_does_not_raise_fault() { bool fallbackActionExecuted = false; Func fallbackAction = () => { fallbackActionExecuted = true; return ResultPrimitive.Substitute; }; @@ -172,7 +172,7 @@ public void Should_not_execute_fallback_when_execute_delegate_does_not_raise_fau } [Fact] - public void Should_not_execute_fallback_when_execute_delegate_raises_fault_not_handled_by_policy() + public void Should_not_execute_fallback_when_executed_delegate_raises_fault_not_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackAction = () => { fallbackActionExecuted = true; return ResultPrimitive.Substitute; }; @@ -187,7 +187,7 @@ public void Should_not_execute_fallback_when_execute_delegate_raises_fault_not_h } [Fact] - public void Should_return_fallback_value_when_execute_delegate_raises_fault_handled_by_policy() + public void Should_return_fallback_value_when_executed_delegate_raises_fault_handled_by_policy() { FallbackPolicy fallbackPolicy = Policy .HandleResult(ResultPrimitive.Fault) @@ -197,7 +197,7 @@ public void Should_return_fallback_value_when_execute_delegate_raises_fault_hand } [Fact] - public void Should_execute_fallback_when_execute_delegate_raises_fault_handled_by_policy() + public void Should_execute_fallback_when_executed_delegate_raises_fault_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackAction = () => { fallbackActionExecuted = true; return ResultPrimitive.Substitute; }; @@ -212,7 +212,7 @@ public void Should_execute_fallback_when_execute_delegate_raises_fault_handled_b } [Fact] - public void Should_execute_fallback_when_execute_delegate_raises_one_of_results_handled_by_policy() + public void Should_execute_fallback_when_executed_delegate_raises_one_of_results_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackAction = () => { fallbackActionExecuted = true; return ResultPrimitive.Substitute; }; @@ -228,7 +228,7 @@ public void Should_execute_fallback_when_execute_delegate_raises_one_of_results_ } [Fact] - public void Should_not_execute_fallback_when_execute_delegate_raises_fault_not_one_of_faults_handled_by_policy() + public void Should_not_execute_fallback_when_executed_delegate_raises_fault_not_one_of_faults_handled_by_policy() { bool fallbackActionExecuted = false; Func fallbackAction = () => { fallbackActionExecuted = true; return ResultPrimitive.Substitute; }; @@ -259,7 +259,7 @@ public void Should_not_execute_fallback_when_result_raised_does_not_match_handli } [Fact] - public void Should_not_execute_fallback_when_execute_delegate_raises_fault_not_handled_by_any_of_predicates() + public void Should_not_execute_fallback_when_executed_delegate_raises_fault_not_handled_by_any_of_predicates() { bool fallbackActionExecuted = false; Func fallbackAction = () => { fallbackActionExecuted = true; return ResultPrimitive.Substitute; }; @@ -352,7 +352,7 @@ public void Should_call_onFallback_passing_result_triggering_fallback() } [Fact] - public void Should_not_call_onFallback_when_execute_delegate_does_not_raise_fault() + public void Should_not_call_onFallback_when_executed_delegate_does_not_raise_fault() { Func fallbackAction = () => ResultPrimitive.Substitute; diff --git a/src/Polly.SharedSpecs/Helpers/PolicyExtensions.cs b/src/Polly.SharedSpecs/Helpers/PolicyExtensions.cs index d731c49096a..82f339cd4a8 100644 --- a/src/Polly.SharedSpecs/Helpers/PolicyExtensions.cs +++ b/src/Polly.SharedSpecs/Helpers/PolicyExtensions.cs @@ -22,7 +22,7 @@ public static void RaiseException(this Policy policy, TException ins NumberOfTimesToRaiseException = 1 }; - policy.RaiseExceptionAndOrCancellation(scenario, new CancellationTokenSource(), () => { }, _ => instance, 0); + policy.RaiseExceptionAndOrCancellation(scenario, new CancellationTokenSource(), () => { }, _ => instance); } public static void RaiseException(this Policy policy, Action configureException = null) where TException : Exception, new() @@ -46,12 +46,12 @@ public static void RaiseException(this Policy policy, TException ins return exception; }; - policy.RaiseExceptionAndOrCancellation(scenario, new CancellationTokenSource(), () => { }, exceptionFactory, 0); + policy.RaiseExceptionAndOrCancellation(scenario, new CancellationTokenSource(), () => { }, exceptionFactory); } public static void RaiseExceptionAndOrCancellation(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute) where TException : Exception, new() { - policy.RaiseExceptionAndOrCancellation(scenario, cancellationTokenSource, onExecute, 0); + policy.RaiseExceptionAndOrCancellation(scenario, cancellationTokenSource, onExecute, _ => new TException()); } public static TResult RaiseExceptionAndOrCancellation(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute, TResult successResult) where TException : Exception, new() @@ -60,6 +60,36 @@ public static void RaiseException(this Policy policy, TException ins _ => new TException(), successResult); } + public static void RaiseExceptionAndOrCancellation(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute, Func exceptionFactory) where TException : Exception + { + int counter = 0; + + CancellationToken cancellationToken = cancellationTokenSource.Token; + + policy.Execute(ct => + { + onExecute(); + + counter++; + + if (scenario.AttemptDuringWhichToCancel.HasValue && counter >= scenario.AttemptDuringWhichToCancel.Value) + { + cancellationTokenSource.Cancel(); + } + + if (scenario.ActionObservesCancellation) + { + ct.ThrowIfCancellationRequested(); + } + + if (counter <= scenario.NumberOfTimesToRaiseException) + { + throw exceptionFactory(counter); + } + + }, cancellationToken); + } + public static TResult RaiseExceptionAndOrCancellation(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute, Func exceptionFactory, TResult successResult) where TException : Exception { int counter = 0; diff --git a/src/Polly.SharedSpecs/Helpers/PolicyExtensionsAsync.cs b/src/Polly.SharedSpecs/Helpers/PolicyExtensionsAsync.cs index bed072b73b8..d48e9254d72 100644 --- a/src/Polly.SharedSpecs/Helpers/PolicyExtensionsAsync.cs +++ b/src/Polly.SharedSpecs/Helpers/PolicyExtensionsAsync.cs @@ -25,7 +25,7 @@ public static Task RaiseExceptionAsync(this Policy policy, TExceptio NumberOfTimesToRaiseException = 1 }; - return policy.RaiseExceptionAndOrCancellationAsync(scenario, new CancellationTokenSource(), () => { }, _ => instance, 0); + return policy.RaiseExceptionAndOrCancellationAsync(scenario, new CancellationTokenSource(), () => { }, _ => instance); } public static Task RaiseExceptionAsync(this Policy policy, Action configureException = null) where TException : Exception, new() @@ -49,12 +49,12 @@ public static Task RaiseExceptionAsync(this Policy policy, TExceptio return exception; }; - return policy.RaiseExceptionAndOrCancellationAsync(scenario, new CancellationTokenSource(), () => { }, exceptionFactory, 0); + return policy.RaiseExceptionAndOrCancellationAsync(scenario, new CancellationTokenSource(), () => { }, exceptionFactory); } public static Task RaiseExceptionAndOrCancellationAsync(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute) where TException : Exception, new() { - return policy.RaiseExceptionAndOrCancellationAsync(scenario, cancellationTokenSource, onExecute, 0); + return policy.RaiseExceptionAndOrCancellationAsync(scenario, cancellationTokenSource, onExecute, _ => new TException()); } public static Task RaiseExceptionAndOrCancellationAsync(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute, TResult successResult) where TException : Exception, new() @@ -63,6 +63,37 @@ public static Task RaiseExceptionAsync(this Policy policy, TExceptio _ => new TException(), successResult); } + public static Task RaiseExceptionAndOrCancellationAsync(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute, Func exceptionFactory) where TException : Exception + { + int counter = 0; + + CancellationToken cancellationToken = cancellationTokenSource.Token; + + return policy.ExecuteAsync(ct => + { + onExecute(); + + counter++; + + if (scenario.AttemptDuringWhichToCancel.HasValue && counter >= scenario.AttemptDuringWhichToCancel.Value) + { + cancellationTokenSource.Cancel(); + } + + if (scenario.ActionObservesCancellation) + { + ct.ThrowIfCancellationRequested(); + } + + if (counter <= scenario.NumberOfTimesToRaiseException) + { + throw exceptionFactory(counter); + } + + return TaskHelper.EmptyTask; + }, cancellationToken); + } + public static Task RaiseExceptionAndOrCancellationAsync(this Policy policy, ExceptionAndOrCancellationScenario scenario, CancellationTokenSource cancellationTokenSource, Action onExecute, Func exceptionFactory, TResult successResult) where TException : Exception { int counter = 0; diff --git a/src/Polly.SharedSpecs/Polly.SharedSpecs.projitems b/src/Polly.SharedSpecs/Polly.SharedSpecs.projitems index 6c48b4bb970..7fc3a25557e 100644 --- a/src/Polly.SharedSpecs/Polly.SharedSpecs.projitems +++ b/src/Polly.SharedSpecs/Polly.SharedSpecs.projitems @@ -80,13 +80,18 @@ + + + + + diff --git a/src/Polly.SharedSpecs/Retry/RetryAsyncSpecs.cs b/src/Polly.SharedSpecs/Retry/RetryAsyncSpecs.cs index 3c6f573059f..38fedf3cd04 100644 --- a/src/Polly.SharedSpecs/Retry/RetryAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Retry/RetryAsyncSpecs.cs @@ -199,6 +199,23 @@ public async Task Should_call_onretry_on_each_retry_with_the_current_exception() .ContainInOrder(expectedExceptions); } + [Fact] + public async Task Should_call_onretry_with_a_handled_innerexception() + { + Exception passedToOnRetry = null; + + var policy = Policy + .HandleInner() + .RetryAsync(3, (exception, _) => passedToOnRetry = exception); + + Exception toRaiseAsInner = new DivideByZeroException(); + Exception withInner = new AggregateException(toRaiseAsInner); + + await policy.RaiseExceptionAsync(withInner); + + passedToOnRetry.Should().BeSameAs(toRaiseAsInner); + } + [Fact] public void Should_not_call_onretry_when_no_retries_are_performed() { diff --git a/src/Polly.SharedSpecs/Retry/RetrySpecs.cs b/src/Polly.SharedSpecs/Retry/RetrySpecs.cs index c3c601f8f43..ef378fc3b81 100644 --- a/src/Polly.SharedSpecs/Retry/RetrySpecs.cs +++ b/src/Polly.SharedSpecs/Retry/RetrySpecs.cs @@ -234,6 +234,23 @@ public void Should_call_onretry_on_each_retry_with_the_current_exception() .ContainInOrder(expectedExceptions); } + [Fact] + public void Should_call_onretry_with_a_handled_innerexception() + { + Exception passedToOnRetry = null; + + var policy = Policy + .HandleInner() + .Retry(3, (exception, _) => passedToOnRetry = exception); + + Exception toRaiseAsInner = new DivideByZeroException(); + Exception withInner = new AggregateException(toRaiseAsInner); + + policy.RaiseException(withInner); + + passedToOnRetry.Should().BeSameAs(toRaiseAsInner); + } + [Fact] public void Should_not_call_onretry_when_no_retries_are_performed() { diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetryAsyncSpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetryAsyncSpecs.cs index 90bb133549c..542bd2eea80 100644 --- a/src/Polly.SharedSpecs/Retry/WaitAndRetryAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetryAsyncSpecs.cs @@ -13,6 +13,7 @@ namespace Polly.Specs.Retry { + [Collection("SystemClockDependantCollection")] public class WaitAndRetryAsyncSpecs : IDisposable { public WaitAndRetryAsyncSpecs() @@ -559,6 +560,67 @@ public async Task Should_calculate_retry_timespans_from_current_retry_attempt_an .ContainInOrder(expectedRetryWaits); } + [Fact] + public async Task Should_be_able_to_pass_handled_exception_to_sleepdurationprovider() + { + object capturedExceptionInstance = null; + + DivideByZeroException exceptionInstance = new DivideByZeroException(); + + var policy = Policy + .Handle() + .WaitAndRetryAsync(5, + sleepDurationProvider:( retries, ex, ctx) => + { + capturedExceptionInstance = ex; + return TimeSpan.FromMilliseconds(0); + }, + onRetryAsync: (ts, i, ctx, task) => + { + return TaskHelper.EmptyTask; + } + ); + + await policy.RaiseExceptionAsync(exceptionInstance); + + capturedExceptionInstance.Should().Be(exceptionInstance); + } + + [Fact] + public async Task Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {new DivideByZeroException(), 2.Seconds()}, + {new ArgumentNullException(), 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .Handle() + .WaitAndRetryAsync(2, + sleepDurationProvider: (retryAttempt, exc, ctx) => + { + return expectedRetryWaits[exc]; + }, + onRetryAsync: (_, timeSpan, __, ___) => + { + actualRetryWaits.Add(timeSpan); + return TaskHelper.EmptyTask; + }); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + await policy.ExecuteAsync(() => { + if (enumerator.MoveNext()) throw enumerator.Current.Key; + return TaskHelper.EmptyTask; + }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + [Fact] public async Task Should_be_able_to_pass_retry_duration_from_execution_to_sleepDurationProvider_via_context() { diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverAsyncSpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverAsyncSpecs.cs index b3597477e8f..17d8e2e7c4a 100644 --- a/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverAsyncSpecs.cs @@ -12,6 +12,7 @@ namespace Polly.Specs.Retry { + [Collection("SystemClockDependantCollection")] public class WaitAndRetryForeverAsyncSpecs : IDisposable { public WaitAndRetryForeverAsyncSpecs() @@ -286,6 +287,42 @@ public async Task Should_calculate_retry_timespans_from_current_retry_attempt_an .ContainInOrder(expectedRetryWaits); } + [Fact] + public async Task Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {new DivideByZeroException(), 2.Seconds()}, + {new ArgumentNullException(), 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .Handle() + .WaitAndRetryForeverAsync( + sleepDurationProvider: (retryAttempt, exc, ctx) => + { + return expectedRetryWaits[exc]; + }, + onRetryAsync: (_, timeSpan, __) => + { + actualRetryWaits.Add(timeSpan); + return TaskHelper.EmptyTask; + }); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + await policy.ExecuteAsync(() => { + if (enumerator.MoveNext()) throw enumerator.Current.Key; + return TaskHelper.EmptyTask; + }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + + [Fact] public async Task Should_be_able_to_pass_retry_duration_from_execution_to_sleepDurationProvider_via_context() { diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverSpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverSpecs.cs index 4deb2d734c9..26530c38830 100644 --- a/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverSpecs.cs +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverSpecs.cs @@ -8,6 +8,7 @@ namespace Polly.Specs.Retry { + [Collection("SystemClockDependantCollection")] public class WaitAndRetryForeverSpecs : IDisposable { public WaitAndRetryForeverSpecs() @@ -34,9 +35,11 @@ public void Should_throw_when_sleep_duration_provider_is_null_with_context() { Action onRetry = (_, __, ___) => { }; + Func sleepDurationProvider = null; + Action policy = () => Policy .Handle() - .WaitAndRetryForever(null, onRetry); + .WaitAndRetryForever(sleepDurationProvider, onRetry); policy.ShouldThrow().And .ParamName.Should().Be("sleepDurationProvider"); @@ -282,6 +285,32 @@ public void Should_calculate_retry_timespans_from_current_retry_attempt_and_time .ContainInOrder(expectedRetryWaits); } + [Fact] + public void Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {new DivideByZeroException(), 2.Seconds()}, + {new ArgumentNullException(), 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .Handle() + .WaitAndRetryForever( + (retryAttempt, exc, ctx) => expectedRetryWaits[exc], + (_, timeSpan, __) => actualRetryWaits.Add(timeSpan) + ); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + policy.Execute(() => { if (enumerator.MoveNext()) throw enumerator.Current.Key; }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + [Fact] public void Should_be_able_to_pass_retry_duration_from_execution_to_sleepDurationProvider_via_context() { diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverTResultAsyncSpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverTResultAsyncSpecs.cs new file mode 100644 index 00000000000..2937f38477b --- /dev/null +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverTResultAsyncSpecs.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using FluentAssertions; +using Polly.Specs.Helpers; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.Retry +{ + [Collection("SystemClockDependantCollection")] + public class WaitAndRetryForeverTResultAsyncSpecs : IDisposable + { + public WaitAndRetryForeverTResultAsyncSpecs() + { + // do nothing on call to sleep + SystemClock.SleepAsync = (_, __) => TaskHelper.EmptyTask; + } + + [Fact] + public async Task Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {ResultPrimitive.Fault, 2.Seconds()}, + {ResultPrimitive.FaultAgain, 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .HandleResult(ResultPrimitive.Fault) + .OrResult(ResultPrimitive.FaultAgain) + .WaitAndRetryForeverAsync( + (retryAttempt, outcome, ctx) => expectedRetryWaits[outcome.Result], + (_, timeSpan, __) => + { + actualRetryWaits.Add(timeSpan); + return TaskHelper.EmptyTask; + }); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + await policy.ExecuteAsync(async () => + { + await TaskHelper.EmptyTask; + if (enumerator.MoveNext()) return enumerator.Current.Key; + else return ResultPrimitive.Undefined; + }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + + public void Dispose() + { + SystemClock.Reset(); + } + + } +} \ No newline at end of file diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverTResultSpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverTResultSpecs.cs new file mode 100644 index 00000000000..94366a3ccf7 --- /dev/null +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetryForeverTResultSpecs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Polly.Specs.Helpers; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.Retry +{ + [Collection("SystemClockDependantCollection")] + public class WaitAndRetryForeverTResultSpecs : IDisposable + { + public WaitAndRetryForeverTResultSpecs() + { + // do nothing on call to sleep + SystemClock.Sleep = (_, __) => { }; + } + + [Fact] + public void Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {ResultPrimitive.Fault, 2.Seconds()}, + {ResultPrimitive.FaultAgain, 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .HandleResult(ResultPrimitive.Fault) + .OrResult(ResultPrimitive.FaultAgain) + .WaitAndRetryForever( + (retryAttempt, outcome, ctx) => expectedRetryWaits[outcome.Result], + (_, timeSpan, __) => actualRetryWaits.Add(timeSpan) + ); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + policy.Execute(() => + { + if (enumerator.MoveNext()) return enumerator.Current.Key; + else return ResultPrimitive.Undefined; + }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + + public void Dispose() + { + SystemClock.Reset(); + } + + } +} \ No newline at end of file diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetrySpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetrySpecs.cs index 124e602b858..98efbe48ffe 100644 --- a/src/Polly.SharedSpecs/Retry/WaitAndRetrySpecs.cs +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetrySpecs.cs @@ -11,6 +11,7 @@ namespace Polly.Specs.Retry { + [Collection("SystemClockDependantCollection")] public class WaitAndRetrySpecs : IDisposable { public WaitAndRetrySpecs() @@ -705,6 +706,57 @@ public void Should_calculate_retry_timespans_from_current_retry_attempt_and_time .ContainInOrder(expectedRetryWaits); } + [Fact] + public void Should_be_able_to_pass_handled_exception_to_sleepdurationprovider() + { + object capturedExceptionInstance = null; + + DivideByZeroException exceptionInstance = new DivideByZeroException(); + + var policy = Policy + .Handle() + .WaitAndRetry(5, + sleepDurationProvider: (retries, ex, ctx) => + { + capturedExceptionInstance = ex; + return TimeSpan.FromMilliseconds(0); + }, + onRetry: (ex, ts, i, ctx) => + { + } + ); + + policy.RaiseException(exceptionInstance); + + capturedExceptionInstance.Should().Be(exceptionInstance); + } + + [Fact] + public void Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {new DivideByZeroException(), 2.Seconds()}, + {new ArgumentNullException(), 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .Handle() + .WaitAndRetry(2, + (retryAttempt, exc, ctx) => expectedRetryWaits[exc], + (_, timeSpan, __, ___) => actualRetryWaits.Add(timeSpan) + ); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + policy.Execute(() => { if (enumerator.MoveNext()) throw enumerator.Current.Key; }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + [Fact] public void Should_be_able_to_pass_retry_duration_from_execution_to_sleepDurationProvider_via_context() { @@ -1137,12 +1189,12 @@ public void Should_honour_and_report_cancellation_during_func_execution() attemptsInvoked.Should().Be(1); } + #endregion + public void Dispose() { SystemClock.Reset(); } - #endregion - } } \ No newline at end of file diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetryTResultAsyncSpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetryTResultAsyncSpecs.cs new file mode 100644 index 00000000000..aff1326ecee --- /dev/null +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetryTResultAsyncSpecs.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using FluentAssertions; +using Polly.Specs.Helpers; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.Retry +{ + [Collection("SystemClockDependantCollection")] + public class WaitAndRetryTResultAsyncSpecs : IDisposable + { + public WaitAndRetryTResultAsyncSpecs() + { + // do nothing on call to sleep + SystemClock.SleepAsync = (_, __) => TaskHelper.EmptyTask; + } + + [Fact] + public async Task Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {ResultPrimitive.Fault, 2.Seconds()}, + {ResultPrimitive.FaultAgain, 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .HandleResult(ResultPrimitive.Fault) + .OrResult(ResultPrimitive.FaultAgain) + .WaitAndRetryAsync(2, + (retryAttempt, outcome, ctx) => expectedRetryWaits[outcome.Result], + (_, timeSpan, __, ___) => + { + actualRetryWaits.Add(timeSpan); + return TaskHelper.EmptyTask; + }); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + await policy.ExecuteAsync(async () => + { + await TaskHelper.EmptyTask; + if (enumerator.MoveNext()) return enumerator.Current.Key; + else return ResultPrimitive.Undefined; + }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + + public void Dispose() + { + SystemClock.Reset(); + } + + } +} \ No newline at end of file diff --git a/src/Polly.SharedSpecs/Retry/WaitAndRetryTResultSpecs.cs b/src/Polly.SharedSpecs/Retry/WaitAndRetryTResultSpecs.cs new file mode 100644 index 00000000000..4c74ad76c93 --- /dev/null +++ b/src/Polly.SharedSpecs/Retry/WaitAndRetryTResultSpecs.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using Polly.Specs.Helpers; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.Retry +{ + [Collection("SystemClockDependantCollection")] + public class WaitAndRetryTResultSpecs : IDisposable + { + public WaitAndRetryTResultSpecs() + { + // do nothing on call to sleep + SystemClock.Sleep = (_, __) => { }; + } + + [Fact] + public void Should_be_able_to_calculate_retry_timespans_based_on_the_handled_fault() + { + Dictionary expectedRetryWaits = new Dictionary(){ + + {ResultPrimitive.Fault, 2.Seconds()}, + {ResultPrimitive.FaultAgain, 4.Seconds()}, + }; + + var actualRetryWaits = new List(); + + var policy = Policy + .HandleResult(ResultPrimitive.Fault) + .OrResult(ResultPrimitive.FaultAgain) + .WaitAndRetry(2, + (retryAttempt, outcome, ctx) => expectedRetryWaits[outcome.Result], + (_, timeSpan, __, ___) => actualRetryWaits.Add(timeSpan) + ); + + using (var enumerator = expectedRetryWaits.GetEnumerator()) + { + policy.Execute(() => + { + if (enumerator.MoveNext()) return enumerator.Current.Key; + else return ResultPrimitive.Undefined; + }); + } + + actualRetryWaits.Should().ContainInOrder(expectedRetryWaits.Values); + } + + public void Dispose() + { + SystemClock.Reset(); + } + + } +} \ No newline at end of file diff --git a/src/Polly.SharedSpecs/Timeout/TimeoutAsyncSpecs.cs b/src/Polly.SharedSpecs/Timeout/TimeoutAsyncSpecs.cs index 2654d3581a6..a3ee6bfd4b9 100644 --- a/src/Polly.SharedSpecs/Timeout/TimeoutAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Timeout/TimeoutAsyncSpecs.cs @@ -10,6 +10,7 @@ namespace Polly.Specs.Timeout { + [Collection("SystemClockDependantCollection")] public class TimeoutAsyncSpecs : TimeoutSpecsBase { #region Configuration diff --git a/src/Polly.SharedSpecs/Timeout/TimeoutSpecs.cs b/src/Polly.SharedSpecs/Timeout/TimeoutSpecs.cs index c0cbecece28..7ec7b274aaa 100644 --- a/src/Polly.SharedSpecs/Timeout/TimeoutSpecs.cs +++ b/src/Polly.SharedSpecs/Timeout/TimeoutSpecs.cs @@ -9,6 +9,7 @@ namespace Polly.Specs.Timeout { + [Collection("SystemClockDependantCollection")] public class TimeoutSpecs : TimeoutSpecsBase { #region Configuration diff --git a/src/Polly.SharedSpecs/Timeout/TimeoutTResultAsyncSpecs.cs b/src/Polly.SharedSpecs/Timeout/TimeoutTResultAsyncSpecs.cs index a9170bff9b2..3423999f3a3 100644 --- a/src/Polly.SharedSpecs/Timeout/TimeoutTResultAsyncSpecs.cs +++ b/src/Polly.SharedSpecs/Timeout/TimeoutTResultAsyncSpecs.cs @@ -10,6 +10,7 @@ namespace Polly.Specs.Timeout { + [Collection("SystemClockDependantCollection")] public class TimeoutTResultAsyncSpecs : TimeoutSpecsBase { #region Configuration diff --git a/src/Polly.SharedSpecs/Timeout/TimeoutTResultSpecs.cs b/src/Polly.SharedSpecs/Timeout/TimeoutTResultSpecs.cs index cb0e0793027..bb914b60597 100644 --- a/src/Polly.SharedSpecs/Timeout/TimeoutTResultSpecs.cs +++ b/src/Polly.SharedSpecs/Timeout/TimeoutTResultSpecs.cs @@ -10,6 +10,7 @@ namespace Polly.Specs.Timeout { + [Collection("SystemClockDependantCollection")] public class TimeoutTResultSpecs : TimeoutSpecsBase { #region Configuration diff --git a/src/Polly.SharedSpecs/Wrap/IPolicyWrapExtensionSpecs.cs b/src/Polly.SharedSpecs/Wrap/IPolicyWrapExtensionSpecs.cs new file mode 100644 index 00000000000..edeae91b559 --- /dev/null +++ b/src/Polly.SharedSpecs/Wrap/IPolicyWrapExtensionSpecs.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using Xunit; +using Polly.Wrap; +using System.Linq; +using FluentAssertions; +using Polly.CircuitBreaker; +using Polly.NoOp; +using Polly.Retry; + +namespace Polly.Specs.Wrap +{ + public class IPolicyWrapExtensionSpecs + { + + [Fact] + public void Should_pass_all_nested_policies_from_PolicyWrap_in_same_order_they_were_added() + { + NoOpPolicy policy0 = Policy.NoOp(); + NoOpPolicy policy1 = Policy.NoOp(); + NoOpPolicy policy2 = Policy.NoOp(); + + PolicyWrap policyWrap = Policy.Wrap(policy0, policy1, policy2); + + List policies = policyWrap.GetPolicies().ToList(); + policies.Count.Should().Be(3); + policies[0].Should().Be(policy0); + policies[1].Should().Be(policy1); + policies[2].Should().Be(policy2); + } + + [Fact] + public void Should_return_sequence_from_GetPolicies() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.NoOp(); + PolicyWrap wrap = Policy.Wrap(policyA, policyB); + + wrap.GetPolicies().ShouldBeEquivalentTo(new[] { policyA, policyB }, options => options.WithStrictOrdering()); + } + + [Fact] + public void Threepolicies_by_static_sequence_should_return_correct_sequence_from_GetPolicies() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.NoOp(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = Policy.Wrap(policyA, policyB, policyC); + + wrap.GetPolicies().ShouldBeEquivalentTo(new[] { policyA, policyB, policyC }, options => options.WithStrictOrdering()); + } + + [Fact] + public void Threepolicies_lefttree_should_return_correct_sequence_from_GetPolicies() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.NoOp(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB).Wrap(policyC); + + wrap.GetPolicies().ShouldBeEquivalentTo(new[] { policyA, policyB, policyC }, options => options.WithStrictOrdering()); + } + + [Fact] + public void Threepolicies_righttree_should_return_correct_sequence_from_GetPolicies() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.NoOp(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicies().ShouldBeEquivalentTo(new[] { policyA, policyB, policyC }, options => options.WithStrictOrdering()); + } + + [Fact] + public void GetPoliciesTPolicy_should_return_single_policy_of_type_TPolicy() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicies().ShouldBeEquivalentTo(new[] { policyB }, options => options.WithStrictOrdering()); + } + + [Fact] + public void GetPoliciesTPolicy_should_return_empty_enumerable_if_no_policy_of_type_TPolicy() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicies().Should().BeEmpty(); + } + + [Fact] + public void GetPoliciesTPolicy_should_return_multiple_policies_of_type_TPolicy() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicies().ShouldBeEquivalentTo(new[] { policyA, policyC }, options => options.WithStrictOrdering()); + } + + [Fact] + public void GetPoliciesTPolicy_should_return_policies_of_type_TPolicy_matching_predicate() + { + CircuitBreakerPolicy policyA = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + Policy policyB = Policy.Handle().Retry(); + CircuitBreakerPolicy policyC = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + + policyA.Isolate(); + + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicies(p => p.CircuitState == CircuitState.Closed).ShouldBeEquivalentTo(new[] { policyC }, options => options.WithStrictOrdering()); + } + + [Fact] + public void GetPoliciesTPolicy_should_return_empty_enumerable_if_none_match_predicate() + { + CircuitBreakerPolicy policyA = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + Policy policyB = Policy.Handle().Retry(); + CircuitBreakerPolicy policyC = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicies(p => p.CircuitState == CircuitState.Open).Should().BeEmpty(); + } + + [Fact] + public void GetPoliciesTPolicy_with_predicate_should_return_multiple_policies_of_type_TPolicy_if_multiple_match_predicate() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicies(_ => true).ShouldBeEquivalentTo(new[] { policyA, policyC }, options => options.WithStrictOrdering()); + } + + [Fact] + public void GetPoliciesTPolicy_with_predicate_should_throw_if_predicate_is_null() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB); + + Action configure = () => wrap.GetPolicies(null); + + configure.ShouldThrow().And.ParamName.Should().Be("filter"); + } + + [Fact] + public void GetPolicyTPolicy_should_return_single_policy_of_type_TPolicy() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicy().Should().BeSameAs(policyB); + } + + [Fact] + public void GetPolicyTPolicy_should_return_null_if_no_TPolicy() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicy().Should().BeNull(); + } + + [Fact] + public void GetPolicyTPolicy_should_throw_if_multiple_TPolicy() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.Invoking(p => p.GetPolicy()).ShouldThrow(); + } + + [Fact] + public void GetPolicyTPolicy_should_return_single_policy_of_type_TPolicy_matching_predicate() + { + CircuitBreakerPolicy policyA = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + Policy policyB = Policy.Handle().Retry(); + CircuitBreakerPolicy policyC = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + + policyA.Isolate(); + + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicy(p => p.CircuitState == CircuitState.Closed).Should().BeSameAs(policyC); + } + + [Fact] + public void GetPolicyTPolicy_should_return_null_if_none_match_predicate() + { + CircuitBreakerPolicy policyA = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + Policy policyB = Policy.Handle().Retry(); + CircuitBreakerPolicy policyC = Policy.Handle().CircuitBreaker(1, TimeSpan.Zero); + + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.GetPolicy(p => p.CircuitState == CircuitState.Open).Should().BeNull(); + } + + [Fact] + public void GetPolicyTPolicy_with_predicate_should_throw_if_multiple_TPolicy_if_multiple_match_predicate() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.Handle().Retry(); + Policy policyC = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB.Wrap(policyC)); + + wrap.Invoking(p => p.GetPolicy(_ => true)).ShouldThrow(); + } + + [Fact] + public void GetPolicyTPolicy_with_predicate_should_throw_if_predicate_is_null() + { + Policy policyA = Policy.NoOp(); + Policy policyB = Policy.NoOp(); + PolicyWrap wrap = policyA.Wrap(policyB); + + Action configure = () => wrap.GetPolicy(null); + + configure.ShouldThrow().And.ParamName.Should().Be("filter"); + } + } +} diff --git a/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecs.cs b/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecs.cs index d4678e9fd07..f59258662d1 100644 --- a/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecs.cs +++ b/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecs.cs @@ -5,6 +5,7 @@ namespace Polly.Specs.Wrap { + [Collection("SystemClockDependantCollection")] public class PolicyWrapContextAndKeySpecs { #region PolicyKey and execution Context tests @@ -114,14 +115,13 @@ public void Should_pass_outmost_PolicyWrap_Key_as_PolicyWrapKey_to_innermost_Pol var outerWrap = fallback.Wrap(innerWrap).WithPolicyKey(outerWrapKey); bool doneOnceOny = false; - outerWrap.Execute(() => + outerWrap.Execute(() => { if (!doneOnceOny) { doneOnceOny = true; throw new Exception(); } - return 0; }); policyWrapKeySetOnExecutionContext.Should().NotBe(retryKey); diff --git a/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecsAsync.cs b/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecsAsync.cs index ce424906abe..1873ec15049 100644 --- a/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecsAsync.cs +++ b/src/Polly.SharedSpecs/Wrap/PolicyWrapContextAndKeySpecsAsync.cs @@ -7,6 +7,7 @@ namespace Polly.Specs.Wrap { + [Collection("SystemClockDependantCollection")] public class PolicyWrapContextAndKeySpecsAsync { #region PolicyKey and execution Context tests @@ -116,14 +117,14 @@ public async Task Should_pass_outmost_PolicyWrap_Key_as_PolicyWrapKey_to_innermo var outerWrap = fallback.WrapAsync(innerWrap).WithPolicyKey(outerWrapKey); bool doneOnceOny = false; - await outerWrap.ExecuteAsync(() => + await outerWrap.ExecuteAsync(() => { if (!doneOnceOny) { doneOnceOny = true; throw new Exception(); } - return TaskHelper.FromResult(0); + return TaskHelper.EmptyTask; }); policyWrapKeySetOnExecutionContext.Should().NotBe(retryKey); diff --git a/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecs.cs b/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecs.cs index b27f962e944..ccdae2645f5 100644 --- a/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecs.cs +++ b/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecs.cs @@ -9,6 +9,7 @@ namespace Polly.Specs.Wrap { + [Collection("SystemClockDependantCollection")] public class PolicyWrapSpecs { #region Instance configuration syntax tests, non-generic policies diff --git a/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecsAsync.cs b/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecsAsync.cs index 97e796a9c28..39f47b118cd 100644 --- a/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecsAsync.cs +++ b/src/Polly.SharedSpecs/Wrap/PolicyWrapSpecsAsync.cs @@ -10,6 +10,7 @@ namespace Polly.Specs.Wrap { + [Collection("SystemClockDependantCollection")] public class PolicyWrapSpecsAsync { #region Instance configuration syntax tests, non-generic policies diff --git a/src/Polly.nuspec b/src/Polly.nuspec index aebe9572910..30931b4fbc1 100644 --- a/src/Polly.nuspec +++ b/src/Polly.nuspec @@ -13,7 +13,17 @@ Exception Handling Resilience Transient Fault Policy Circuit Breaker CircuitBreaker Retry Wait Cache Cache-aside Bulkhead Fallback Timeout Throttle Parallelization Copyright © 2017, App vNext - v5.0 is a major release with significant new resilience policies: Timeout; Bulkhead Isolation; Fallback; Cache; and PolicyWrap. See release notes back to v5.0.0 for full details. v5.0.5 includes important circuit-breaker fixes. + v5.0 is a major release with significant new resilience policies: Timeout; Bulkhead Isolation; Fallback; Cache; and PolicyWrap. See release notes back to v5.0.0 for full details. + + 5.6.0 + --------------------- + - Add ability to handle inner exceptions natively: .HandleInner<TEx>() + - Allow WaitAndRetry policies to calculate wait based on the handled fault + - Add the ability to access the policies within an IPolicyWrap + - Allow PolicyWrap to take interfaces as parameters + - Bug fix: set context keys for generic execute method with PolicyWrap + - Bug fix: generic TResult method with non-generic fallback policy + - Performance improvements 5.5.0 ---------------------