diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf2cfeb2c3..bff3bfba59a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.3.0 + +- Add RateLimit policy - Thanks to [@reisenberger](https://github.com/reisenberger) + ## 7.2.2 - Recursively search all `AggregateException` inner exceptions for predicate matches when using `HandleInner()` ([#818](https://github.com/App-vNext/Polly/issues/818)) - Thanks to [@sideproject](https://github.com/sideproject) diff --git a/GitVersionConfig.yaml b/GitVersionConfig.yaml index f94af5baaac..0105503e461 100644 --- a/GitVersionConfig.yaml +++ b/GitVersionConfig.yaml @@ -1 +1 @@ -next-version: 7.2.2 +next-version: 7.3.0 diff --git a/README.md b/README.md index 7b7ab5925cc..806e7ed90ab 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Polly -Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. +Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, Rate-limiting and Fallback in a fluent and thread-safe manner. -Polly targets .NET Standard 1.1 ([coverage](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support): .NET Core 1.0, Mono, Xamarin, UWP, WP8.1+) and .NET Standard 2.0+ ([coverage](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support): .NET Core 2.0+, .NET Core 3.0, and later Mono, Xamarin and UWP targets). The nuget package also includes direct targets for .NET Framework 4.6.1 and 4.7.2. +Polly targets .NET Standard 1.1 ([coverage](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support): .NET Core 1.0, Mono, Xamarin, UWP, WP8.1+) and .NET Standard 2.0+ ([coverage](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support): .NET Core 2.0+, .NET Core 3.0, and later Mono, Xamarin and UWP targets). The NuGet package also includes direct targets for .NET Framework 4.6.1 and 4.7.2. For versions supporting earlier targets such as .NET4.0 and .NET3.5, see the [supported targets](https://github.com/App-vNext/Polly/wiki/Supported-targets) grid. @@ -27,23 +27,24 @@ Polly offers multiple resilience policies: |Policy| Premise | Aka| How does the policy mitigate?| | ------------- | ------------- |:-------------: |------------- | |**Retry**
(policy family)
([quickstart](#retry) ; [deep](https://github.com/App-vNext/Polly/wiki/Retry))|Many faults are transient and may self-correct after a short delay.| "Maybe it's just a blip" | Allows configuring automatic retries. | -|**Circuit-breaker**
(policy family)
([quickstart](#circuit-breaker) ; [deep](https://github.com/App-vNext/Polly/wiki/Circuit-Breaker))|When a system is seriously struggling, failing fast is better than making users/callers wait.

Protecting a faulting system from overload can help it recover. | "Stop doing it if it hurts"

"Give that system a break" | Breaks the circuit (blocks executions) for a period, when faults exceed some pre-configured threshold. | -|**Timeout**
([quickstart](#timeout) ; [deep](https://github.com/App-vNext/Polly/wiki/Timeout))|Beyond a certain wait, a success result is unlikely.| "Don't wait forever" |Guarantees the caller won't have to wait beyond the timeout. | -|**Bulkhead Isolation**
([quickstart](#bulkhead) ; [deep](https://github.com/App-vNext/Polly/wiki/Bulkhead))|When a process faults, multiple failing calls backing up can easily swamp resource (eg threads/CPU) in a host.

A faulting downstream system can also cause 'backed-up' failing calls upstream.

Both risk a faulting process bringing down a wider system. | "One fault shouldn't sink the whole ship" |Constrains the governed actions to a fixed-size resource pool, isolating their potential to affect others. | +|**Circuit-breaker**
(policy family)
([quickstart](#circuit-breaker) ; [deep](https://github.com/App-vNext/Polly/wiki/Circuit-Breaker))|When a system is seriously struggling, failing fast is better than making users/callers wait.

Protecting a faulting system from overload can help it recover. | "Stop doing it if it hurts"

"Give that system a break" | Breaks the circuit (blocks executions) for a period, when faults exceed some pre-configured threshold. | +|**Timeout**
([quickstart](#timeout) ; [deep](https://github.com/App-vNext/Polly/wiki/Timeout))|Beyond a certain wait, a success result is unlikely.| "Don't wait forever" |Guarantees the caller won't have to wait beyond the timeout. | +|**Bulkhead Isolation**
([quickstart](#bulkhead) ; [deep](https://github.com/App-vNext/Polly/wiki/Bulkhead))|When a process faults, multiple failing calls can stack up (if unbounded) and can easily swamp resource (threads/ CPU/ memory) in a host.

This can affect performance more widely by starving other operations of resource, bringing down the host, or causing cascading failures upstream. | "One fault shouldn't sink the whole ship" |Constrains the governed actions to a fixed-size resource pool, isolating their potential to affect others. | +|**Rate-limit**
([quickstart](#rate-limit) ; [deep](https://github.com/App-vNext/Polly/wiki/Rate-Limit))|Limiting the rate a system handles requests is another way to control load.

This can apply to the way your system accepts incoming calls, and/or to the way you call downstream services. | "Slow down a bit, will you?" |Constrains executions to not exceed a certain rate. | |**Cache**
([quickstart](#cache) ; [deep](https://github.com/App-vNext/Polly/wiki/Cache))|Some proportion of requests may be similar.| "You've asked that one before" |Provides a response from cache if known.

Stores responses automatically in cache, when first retrieved. | -|**Fallback**
([quickstart](#fallback) ; [deep](https://github.com/App-vNext/Polly/wiki/Fallback))|Things will still fail - plan what you will do when that happens.| "Degrade gracefully" |Defines an alternative value to be returned (or action to be executed) on failure. | -|**PolicyWrap**
([quickstart](#policywrap) ; [deep](https://github.com/App-vNext/Polly/wiki/PolicyWrap))|Different faults require different strategies; resilience means using a combination.| "Defence in depth" |Allows any of the above policies to be combined flexibly. | +|**Fallback**
([quickstart](#fallback) ; [deep](https://github.com/App-vNext/Polly/wiki/Fallback))|Things will still fail - plan what you will do when that happens.| "Degrade gracefully" |Defines an alternative value to be returned (or action to be executed) on failure. | +|**PolicyWrap**
([quickstart](#policywrap) ; [deep](https://github.com/App-vNext/Polly/wiki/PolicyWrap))|Different faults require different strategies; resilience means using a combination.| "Defence in depth" |Allows any of the above policies to be combined flexibly. | In addition to the detailed pages on each policy, an [introduction to the role of each policy in resilience engineering](https://github.com/App-vNext/Polly/wiki/Transient-fault-handling-and-proactive-resilience-engineering) is also provided in the wiki. ### Using Polly with HttpClient factory from ASPNET Core 2.1 -For using Polly with HttpClient factory from ASPNET Core 2.1, see our [detailed wiki page](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory), then come back here or [explore the wiki](https://github.com/App-vNext/Polly/wiki) to learn more about the operation of each policy. +For using Polly with HttpClient factory from ASP.NET Core 2.1, see our [detailed wiki page](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory), then come back here or [explore the wiki](https://github.com/App-vNext/Polly/wiki) to learn more about the operation of each policy. ### Release notes + The [change log](https://github.com/App-vNext/Polly/blob/master/CHANGELOG.md) describes changes by release. -+ We tag Pull Requests and Issues with [milestones](https://github.com/App-vNext/Polly/milestones) which match to nuget package release numbers. ++ We tag Pull Requests and Issues with [milestones](https://github.com/App-vNext/Polly/milestones) which match to NuGet package release numbers. + Breaking changes are called out in the wiki ([v7](https://github.com/App-vNext/Polly/wiki/Polly-v7-breaking-changes) ; [v6](https://github.com/App-vNext/Polly/wiki/Polly-v6-breaking-changes)) with simple notes on any necessary steps to upgrade. ### Supported targets @@ -58,7 +59,7 @@ This ReadMe aims to give a quick overview of all Polly features - including enou Fault-handling policies handle specific exceptions thrown by, or results returned by, the delegates you execute through the policy. -## Step 1 : Specify the exceptions/faults you want the policy to handle +## Step 1 : Specify the exceptions/faults you want the policy to handle ```csharp // Single exception type @@ -91,11 +92,11 @@ Policy From Polly v4.3.0 onwards, policies wrapping calls returning a `TResult` can also handle `TResult` return values: ```csharp -// Handle return value with condition +// Handle return value with condition Policy .HandleResult(r => r.StatusCode == HttpStatusCode.NotFound) -// Handle multiple return values +// Handle multiple return values Policy .HandleResult(r => r.StatusCode == HttpStatusCode.InternalServerError) .OrResult(r => r.StatusCode == HttpStatusCode.BadGateway) @@ -104,7 +105,7 @@ Policy Policy .HandleResult(HttpStatusCode.InternalServerError) .OrResult(HttpStatusCode.BadGateway) - + // Handle both exceptions and return values in one policy HttpStatusCode[] httpStatusCodesWorthRetrying = { HttpStatusCode.RequestTimeout, // 408 @@ -112,7 +113,7 @@ HttpStatusCode[] httpStatusCodesWorthRetrying = { HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout // 504 -}; +}; HttpResponseMessage result = await Policy .Handle() .OrResult(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode)) @@ -120,11 +121,11 @@ HttpResponseMessage result = await Policy .ExecuteAsync( /* some Func> */ ) ``` -For more information, see [Handling Return Values](#handing-return-values-and-policytresult) at foot of this readme. +For more information, see [Handling Return Values](#handing-return-values-and-policytresult) at foot of this readme. ## Step 2 : Specify how the policy should handle those faults -### Retry +### Retry ```csharp // Retry once @@ -137,7 +138,7 @@ Policy .Handle() .Retry(3) -// Retry multiple times, calling an action on each retry +// Retry multiple times, calling an action on each retry // with the current exception and retry count Policy .Handle() @@ -146,14 +147,14 @@ Policy // Add logic to be executed before each retry, such as logging }); -// Retry multiple times, calling an action on each retry -// with the current exception, retry count and context +// Retry multiple times, calling an action on each retry +// with the current exception, retry count and context // provided to Execute() Policy .Handle() .Retry(3, onRetry: (exception, retryCount, context) => { - // Add logic to be executed before each retry, such as logging + // Add logic to be executed before each retry, such as logging }); ``` @@ -166,7 +167,7 @@ Policy .Handle() .RetryForever() -// Retry forever, calling an action on each retry with the +// Retry forever, calling an action on each retry with the // current exception Policy .Handle() @@ -185,10 +186,10 @@ Policy }); ``` -### Wait and retry +### Wait and retry ```csharp -// Retry, waiting a specified duration between each retry. +// Retry, waiting a specified duration between each retry. // (The wait is imposed on catching the failure, before making the next try.) Policy .Handle() @@ -199,7 +200,7 @@ Policy TimeSpan.FromSeconds(3) }); -// Retry, waiting a specified duration between each retry, +// Retry, waiting a specified duration between each retry, // calling an action on each retry with the current exception // and duration Policy @@ -211,10 +212,10 @@ Policy TimeSpan.FromSeconds(3) }, (exception, timeSpan) => { // Add logic to be executed before each retry, such as logging - }); + }); -// Retry, waiting a specified duration between each retry, -// calling an action on each retry with the current exception, +// Retry, waiting a specified duration between each retry, +// calling an action on each retry with the current exception, // duration and context provided to Execute() Policy .Handle() @@ -227,8 +228,8 @@ Policy // Add logic to be executed before each retry, such as logging }); -// Retry, waiting a specified duration between each retry, -// calling an action on each retry with the current exception, +// Retry, waiting a specified duration between each retry, +// calling an action on each retry with the current exception, // duration, retry count, and context provided to Execute() Policy .Handle() @@ -241,9 +242,9 @@ Policy // Add logic to be executed before each retry, such as logging }); -// Retry a specified number of times, using a function to -// calculate the duration to wait between retries based on -// the current retry attempt (allows for exponential backoff) +// Retry a specified number of times, using a function to +// calculate the duration to wait between retries based on +// the current retry attempt (allows for exponential back-off) // In this case will wait for // 2 ^ 1 = 2 seconds then // 2 ^ 2 = 4 seconds then @@ -252,35 +253,35 @@ Policy // 2 ^ 5 = 32 seconds Policy .Handle() - .WaitAndRetry(5, retryAttempt => - TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + .WaitAndRetry(5, retryAttempt => + TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) ); -// Retry a specified number of times, using a function to -// calculate the duration to wait between retries based on -// the current retry attempt, calling an action on each retry -// with the current exception, duration and context provided +// Retry a specified number of times, using a function to +// calculate the duration to wait between retries based on +// the current retry attempt, calling an action on each retry +// with the current exception, duration and context provided // to Execute() Policy .Handle() .WaitAndRetry( - 5, - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + 5, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timeSpan, context) => { // Add logic to be executed before each retry, such as logging } ); -// Retry a specified number of times, using a function to -// calculate the duration to wait between retries based on -// the current retry attempt, calling an action on each retry -// with the current exception, duration, retry count, and context +// Retry a specified number of times, using a function to +// calculate the duration to wait between retries based on +// the current retry attempt, calling an action on each retry +// with the current exception, duration, retry count, and context // provided to Execute() Policy .Handle() .WaitAndRetry( - 5, - retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), + 5, + retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timeSpan, retryCount, context) => { // Add logic to be executed before each retry, such as logging } @@ -291,18 +292,18 @@ The above code demonstrates how to build common wait-and-retry patterns from scr For `WaitAndRetry` policies handling Http Status Code 429 Retry-After, see [wiki documentation](https://github.com/App-vNext/Polly/wiki/Retry#retryafter-when-the-response-specifies-how-long-to-wait). -### Wait and retry forever (until succeeds) +### Wait and retry forever (until succeeds) ```csharp // Wait and retry forever Policy .Handle() - .WaitAndRetryForever(retryAttempt => + .WaitAndRetryForever(retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) ); -// Wait and retry forever, calling an action on each retry with the +// Wait and retry forever, calling an action on each retry with the // current exception and the time to wait Policy .Handle() @@ -330,7 +331,7 @@ If all retries fail, a retry policy rethrows the final exception back to the cal For more depth see also: [Retry policy documentation on wiki](https://github.com/App-vNext/Polly/wiki/Retry). ### Circuit Breaker - + ```csharp // Break the circuit after the specified number of consecutive exceptions // and keep circuit broken for the specified duration. @@ -368,22 +369,22 @@ CircuitState.Isolated - Circuit held manually in an open state. Execution of act */ // Manually open (and hold open) a circuit breaker - for example to manually isolate a downstream service. -breaker.Isolate(); +breaker.Isolate(); // Reset the breaker to closed state, to start accepting actions again. -breaker.Reset(); +breaker.Reset(); ``` Circuit-breaker policies block exceptions by throwing `BrokenCircuitException` when the circuit is broken. See: [Circuit-Breaker documentation on wiki](https://github.com/App-vNext/Polly/wiki/Circuit-Breaker). -Note that circuit-breaker policies [rethrow all exceptions](https://github.com/App-vNext/Polly/wiki/Circuit-Breaker#exception-handling), even handled ones. A circuit-breaker exists to measure faults and break the circuit when too many faults occur, but does not orchestrate retries. Combine a circuit-breaker with a retry policy as needed. +Note that circuit-breaker policies [rethrow all exceptions](https://github.com/App-vNext/Polly/wiki/Circuit-Breaker#exception-handling), even handled ones. A circuit-breaker exists to measure faults and break the circuit when too many faults occur, but does not orchestrate retries. Combine a circuit-breaker with a retry policy as needed. -### Advanced Circuit Breaker +### Advanced Circuit Breaker ```csharp -// Break the circuit if, within any period of duration samplingDuration, -// the proportion of actions resulting in a handled exception exceeds failureThreshold, +// Break the circuit if, within any period of duration samplingDuration, +// the proportion of actions resulting in a handled exception exceeds failureThreshold, // provided also that the number of actions through the circuit in the period // is at least minimumThroughput. @@ -411,7 +412,7 @@ For more information on the Circuit Breaker pattern in general see: * [Original Circuit Breaking Link](https://web.archive.org/web/20160106203951/http://thatextramile.be/blog/2008/05/the-circuit-breaker) -### Fallback +### Fallback ```csharp // Provide a substitute value, if an execution faults. Policy @@ -428,7 +429,7 @@ Policy // Specify a substitute value or func, calling an action (eg for logging) if the fallback is invoked. Policy .Handle() - .Fallback(UserAvatar.Blank, onFallback: (exception, context) => + .Fallback(UserAvatar.Blank, onFallback: (exception, context) => { // Add extra logic to be called when the fallback is invoked, such as logging }); @@ -499,8 +500,8 @@ Policy Defining and consuming the policy in the same scope, as shown above, is the most immediate way to use Polly. Consider also: + Separate policy definition from policy consumption, and inject policies into the code which will consume them. This [enables many unit-testing scenarios](https://github.com/App-vNext/Polly/wiki/Unit-testing-with-Polly---with-examples). -+ If your application uses Polly in a number of locations, define all policies at startup, and place them in a [`PolicyRegistry`](https://github.com/App-vNext/Polly/wiki/PolicyRegistry). This is a common pattern in .NET Core applications. For instance, you might define your own extension method on `IServiceCollection` to configure the policies you will consume elsewhere in the application. PolicyRegistry also [combines well with DI to support unit-testing](https://github.com/App-vNext/Polly/wiki/Unit-testing-with-Polly---with-examples#use-policyregistry-with-di-to-work-with-policy-collections). - ++ If your application uses Polly in a number of locations, define all policies at start-up, and place them in a [`PolicyRegistry`](https://github.com/App-vNext/Polly/wiki/PolicyRegistry). This is a common pattern in .NET Core applications. For instance, you might define your own extension method on `IServiceCollection` to configure the policies you will consume elsewhere in the application. PolicyRegistry also [combines well with DI to support unit-testing](https://github.com/App-vNext/Polly/wiki/Unit-testing-with-Polly---with-examples#use-policyregistry-with-di-to-work-with-policy-collections). + ```csharp public static ConfigurePollyPolicies(this IServiceCollection services) { @@ -544,21 +545,21 @@ Policy // Timeout, calling an action if the action times out Policy - .Timeout(30, onTimeout: (context, timespan, task) => + .Timeout(30, onTimeout: (context, timespan, task) => { - // Add extra logic to be invoked when a timeout occurs, such as logging + // Add extra logic to be invoked when a timeout occurs, such as logging }); // Eg timeout, logging that the execution timed out: Policy - .Timeout(30, onTimeout: (context, timespan, task) => + .Timeout(30, onTimeout: (context, timespan, task) => { logger.Warn($"{context.PolicyKey} at {context.OperationKey}: execution timed out after {timespan.TotalSeconds} seconds."); }); // Eg timeout, capturing any exception from the timed-out task when it completes: Policy - .Timeout(30, onTimeout: (context, timespan, task) => + .Timeout(30, onTimeout: (context, timespan, task) => { task.ContinueWith(t => { if (t.IsFaulted) logger.Error($"{context.PolicyKey} at {context.OperationKey}: execution timed out after {timespan.TotalSeconds} seconds, with: {t.Exception}."); @@ -573,16 +574,16 @@ Policy timeoutPolicy = Policy.TimeoutAsync(30); HttpResponseMessage httpResponse = await timeoutPolicy .ExecuteAsync( async ct => await httpClient.GetAsync(endpoint, ct), // Execute a delegate which responds to a CancellationToken input parameter. - CancellationToken.None // In this case, CancellationToken.None is passed into the execution, indicating you have no independent cancellation control you wish to add to the cancellation provided by TimeoutPolicy. Your own indepdent CancellationToken can also be passed - see wiki for examples. + CancellationToken.None // In this case, CancellationToken.None is passed into the execution, indicating you have no independent cancellation control you wish to add to the cancellation provided by TimeoutPolicy. Your own independent CancellationToken can also be passed - see wiki for examples. ); ``` #### Pessimistic timeout -Pessimistic timeout allows calling code to 'walk away' from waiting for an executed delegate to complete, even if it does not support cancellation. In synchronous executions this is at the expense of an extra thread; see [deep doco on wiki](https://github.com/App-vNext/Polly/wiki/Timeout#pessimistic-timeout) for more detail. +Pessimistic timeout allows calling code to 'walk away' from waiting for an executed delegate to complete, even if it does not support cancellation. In synchronous executions this is at the expense of an extra thread; see [deep documentation on wiki](https://github.com/App-vNext/Polly/wiki/Timeout#pessimistic-timeout) for more detail. ```csharp -// Timeout after 30 seconds, if the executed delegate has not completed. Enforces this timeout even if the executed code has no cancellation mechanism. +// Timeout after 30 seconds, if the executed delegate has not completed. Enforces this timeout even if the executed code has no cancellation mechanism. Policy .Timeout(30, TimeoutStrategy.Pessimistic) @@ -599,7 +600,7 @@ var response = await timeoutPolicy ); ``` -Timeout policies throw `TimeoutRejectedException` when timeout occurs. +Timeout policies throw `TimeoutRejectedException` when timeout occurs. For more detail see: [Timeout policy documentation](https://github.com/App-vNext/Polly/wiki/Timeout) on wiki. @@ -610,16 +611,16 @@ For more detail see: [Timeout policy documentation](https://github.com/App-vNext Policy .Bulkhead(12) -// Restrict executions through the policy to a maximum of twelve concurrent actions, +// Restrict executions through the policy to a maximum of twelve concurrent actions, // with up to two actions waiting for an execution slot in the bulkhead if all slots are taken. Policy .Bulkhead(12, 2) // Restrict concurrent executions, calling an action if an execution is rejected Policy - .Bulkhead(12, context => + .Bulkhead(12, context => { - // Add callback logic for when the bulkhead rejects execution, such as logging + // Add callback logic for when the bulkhead rejects execution, such as logging }); // Monitor the bulkhead available capacity, for example for health/load reporting. @@ -630,11 +631,71 @@ int freeQueueSlots = bulkhead.QueueAvailableCount; ``` -Bulkhead policies throw `BulkheadRejectedException` if items are queued to the bulkhead when the bulkhead execution and queue are both full. +Bulkhead policies throw `BulkheadRejectedException` if items are queued to the bulkhead when the bulkhead execution and queue are both full. For more detail see: [Bulkhead policy documentation](https://github.com/App-vNext/Polly/wiki/Bulkhead) on wiki. +### Rate-Limit + +```csharp +// Allow up to 20 executions per second. +Policy.RateLimit(20, TimeSpan.FromSeconds(1)); + +// Allow up to 20 executions per second with a burst of 10 executions. +Policy.RateLimit(20, TimeSpan.FromSeconds(1), 10); + +// Allow up to 20 executions per second, with a delegate to return the +// retry-after value to use if the rate limit is exceeded. +Policy.RateLimit(20, TimeSpan.FromSeconds(1), (retryAfter, context) => +{ + return retryAfter.Add(TimeSpan.FromSeconds(2)); +}); + +// Allow up to 20 executions per second with a burst of 10 executions, +// with a delegate to return the retry-after value to use if the rate +// limit is exceeded. +Policy.RateLimit(20, TimeSpan.FromSeconds(1), 10, (retryAfter, context) => +{ + return retryAfter.Add(TimeSpan.FromSeconds(2)); +}); +``` + +Example execution: + +```csharp +public async Task SearchAsync(string query, HttpContext httpContext) +{ + var rateLimit = Policy.RateLimitAsync(20, TimeSpan.FromSeconds(1), 10); + + try + { + var result = await rateLimit.ExecuteAsync(() => TextSearchAsync(query)); + + var json = JsonConvert.SerializeObject(result); + + httpContext.Response.ContentType = "application/json"; + await httpContext.Response.WriteAsync(json); + } + catch (RateLimitRejectedException ex) + { + string retryAfter = DateTimeOffset.UtcNow + .Add(ex.RetryAfter) + .ToUnixTimeSeconds() + .ToString(CultureInfo.InvariantCulture); + + httpContext.Response.StatusCode = 429; + httpContext.Response.Headers["Retry-After"] = retryAfter; + } +} +``` + +Rate-limit policies throw `RateLimitRejectedException` if too many requests are executed within the configured timespan. + +For more detail see: [Rate-limit policy documentation](https://github.com/App-vNext/Polly/wiki/Rate-Limit) in the wiki. + +
+ ### Cache ```csharp @@ -644,7 +705,7 @@ var cachePolicy = Policy.Cache(memoryCacheProvider, TimeSpan.FromMinutes(5)); // For .NET Core DI examples see the CacheProviders linked to from https://github.com/App-vNext/Polly/wiki/Cache#working-with-cacheproviders : // - https://github.com/App-vNext/Polly.Caching.MemoryCache -// - https://github.com/App-vNext/Polly.Caching.IDistributedCache +// - https://github.com/App-vNext/Polly.Caching.IDistributedCache // Define a cache policy with absolute expiration at midnight tonight. var cachePolicy = Policy.Cache(memoryCacheProvider, new AbsoluteTtl(DateTimeOffset.Now.Date.AddDays(1)); @@ -652,14 +713,14 @@ var cachePolicy = Policy.Cache(memoryCacheProvider, new AbsoluteTtl(DateTimeOffs // Define a cache policy with sliding expiration: items remain valid for another 5 minutes each time the cache item is used. var cachePolicy = Policy.Cache(memoryCacheProvider, new SlidingTtl(TimeSpan.FromMinutes(5)); -// Define a cache Policy, and catch any cache provider errors for logging. -var cachePolicy = Policy.Cache(myCacheProvider, TimeSpan.FromMinutes(5), - (context, key, ex) => { - logger.Error($"Cache provider, for key {key}, threw exception: {ex}."); // (for example) +// Define a cache Policy, and catch any cache provider errors for logging. +var cachePolicy = Policy.Cache(myCacheProvider, TimeSpan.FromMinutes(5), + (context, key, ex) => { + logger.Error($"Cache provider, for key {key}, threw exception: {ex}."); // (for example) } ); -// Execute through the cache as a read-through cache: check the cache first; if not found, execute underlying delegate and store the result in the cache. +// Execute through the cache as a read-through cache: check the cache first; if not found, execute underlying delegate and store the result in the cache. // The key to use for caching, for a particular execution, is specified by setting the OperationKey (before v6: ExecutionKey) on a Context instance passed to the execution. Use an overload of the form shown below (or a richer overload including the same elements). // Example: "FooKey" is the cache key that will be used in the below execution. TResult result = cachePolicy.Execute(context => getFoo(), new Context("FooKey")); @@ -702,7 +763,7 @@ For more detail see: [PolicyWrap documentation](https://github.com/App-vNext/Pol ```csharp // Define a policy which will simply cause delegates passed for execution to be executed 'as is'. -// This is useful for stubbing-out Polly in unit tests, +// This is useful for stubbing-out Polly in unit tests, // or in application situations where your code architecture might expect a policy // but you simply want to pass the execution through without policy intervention. NoOpPolicy noOp = Policy.NoOp(); @@ -732,7 +793,7 @@ policyResult.Result - if executing a func, the result if the call succeeded or t # Handing return values, and Policy<TResult> -As described at step 1b, from Polly v4.3.0 onwards, policies can handle return values and exceptions in combination: +As described at step 1b, from Polly v4.3.0 onwards, policies can handle return values and exceptions in combination: ```csharp // Handle both exceptions and return values in one policy @@ -742,7 +803,7 @@ HttpStatusCode[] httpStatusCodesWorthRetrying = { HttpStatusCode.BadGateway, // 502 HttpStatusCode.ServiceUnavailable, // 503 HttpStatusCode.GatewayTimeout // 504 -}; +}; HttpResponseMessage result = await Policy .Handle() .OrResult(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode)) @@ -756,7 +817,7 @@ The exceptions and return results to handle can be expressed fluently in any ord Configuring a policy with `.HandleResult(...)` or `.OrResult(...)` generates a strongly-typed `Policy` of the specific policy type, eg `Retry`, `AdvancedCircuitBreaker`. -These policies must be used to execute delegates returning `TResult`, ie: +These policies must be used to execute delegates returning `TResult`, i.e.: * `Execute(Func)` (and related overloads) * `ExecuteAsync(Func>)` (and related overloads) @@ -775,7 +836,7 @@ policyResult.Result - if executing a func, the result if the call succeeded; oth `.ExecuteAndCapture(Func)` on strongly-typed policies adds two properties: ``` -policyResult.FaultType - was the final fault handled an exception or a result handled by the policy? Will be null if the delegate execution succeeded. +policyResult.FaultType - was the final fault handled an exception or a result handled by the policy? Will be null if the delegate execution succeeded. policyResult.FinalHandledResult - the final fault result handled; will be null or the type's default value, if the call succeeded ``` @@ -787,13 +848,13 @@ In generic-policies handling `TResult` return values, state-change delegates are * `Exception // The exception just thrown if policy is in process of handling an exception (otherwise null)` * `Result // The TResult just raised, if policy is in process of handling a result (otherwise default(TResult))` - + ### BrokenCircuitException<TResult> Non-generic CircuitBreaker policies throw a `BrokenCircuitException` when the circuit is broken. This `BrokenCircuitException` contains the last exception (the one which caused the circuit to break) as the `InnerException`. -For `CircuitBreakerPolicy` policies: +For `CircuitBreakerPolicy` policies: * A circuit broken due to an exception throws a `BrokenCircuitException` with `InnerException` set to the exception which triggered the break (as previously). * A circuit broken due to handling a result throws a `BrokenCircuitException` with the `Result` property set to the result which caused the circuit to break. @@ -816,11 +877,11 @@ var policy = Policy // Identify call sites with an OperationKey, by passing in a Context var customerDetails = policy.Execute(myDelegate, new Context("GetCustomerDetails")); -// "MyDataAccessPolicy" -> context.PolicyKey +// "MyDataAccessPolicy" -> context.PolicyKey // "GetCustomerDetails -> context.OperationKey -// Pass additional custom information from call site into execution context +// Pass additional custom information from call site into execution context var policy = Policy .Handle() .Retry(3, onRetry: (exception, retryCount, context) => @@ -830,7 +891,7 @@ var policy = Policy .WithPolicyKey("MyDataAccessPolicy"); int id = ... // customer id from somewhere -var customerDetails = policy.Execute(context => GetCustomer(id), +var customerDetails = policy.Execute(context => GetCustomer(id), new Context("GetCustomerDetails", new Dictionary() {{"Type","Customer"},{"Id",id}} )); ``` @@ -840,7 +901,7 @@ For more detail see: [Keys and Context Data](https://github.com/App-vNext/Polly/ # PolicyRegistry ```csharp -// Create a policy registry (for example on application start-up) +// Create a policy registry (for example on application start-up) PolicyRegistry registry = new PolicyRegistry(); // Populate the registry with policies @@ -849,12 +910,12 @@ registry.Add("StandardHttpResilience", myStandardHttpResiliencePolicy); registry["StandardHttpResilience"] = myStandardHttpResiliencePolicy; // Pass the registry instance to usage sites by DI, perhaps -public class MyServiceGateway +public class MyServiceGateway { public void MyServiceGateway(..., IReadOnlyPolicyRegistry registry, ...) { ... - } + } } // (Or if you prefer ambient-context pattern, use a thread-safe singleton) @@ -916,14 +977,14 @@ The token you pass as the `cancellationToken` parameter to the `ExecuteAsync(... + In common with the Base Class Library implementation in `Task.Run(…)` and elsewhere, if the cancellation token is cancelled before execution begins, the user delegate is not executed at all. ```csharp -// Try several times to retrieve from a uri, but support cancellation at any time. +// Try several times to retrieve from a URI, but support cancellation at any time. CancellationToken cancellationToken = // ... var policy = Policy .Handle() - .WaitAndRetryAsync(new[] { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(2), - TimeSpan.FromSeconds(4) + .WaitAndRetryAsync(new[] { + TimeSpan.FromSeconds(1), + TimeSpan.FromSeconds(2), + TimeSpan.FromSeconds(4) }); var response = await policy.ExecuteAsync(ct => httpClient.GetAsync(uri, ct), cancellationToken); ``` @@ -932,7 +993,7 @@ From Polly v5.0, synchronous executions also support cancellation via `Cancellat # Thread safety -All Polly policies are fully thread-safe. You can safely re-use policies at multiple call sites, and execute through policies concurrently on different threads. +All Polly policies are fully thread-safe. You can safely re-use policies at multiple call sites, and execute through policies concurrently on different threads. While the internal operation of the policy is thread-safe, this does not magically make delegates you execute through the policy thread-safe: if delegates you execute through the policy are not thread-safe, they remain not thread-safe. @@ -949,7 +1010,7 @@ Execution interfaces [`ISyncPolicy`](https://github.com/App-vNext/Polly/tree/mas Orthogonal to the execution interfaces, interfaces specific to the kind of Policy define properties and methods common to that type of policy. -For example, [`ICircuitBreakerPolicy`](https://github.com/App-vNext/Polly/tree/master/src/Polly.Shared/CircuitBreaker/ICircuitBreakerPolicy.cs) defines +For example, [`ICircuitBreakerPolicy`](https://github.com/App-vNext/Polly/tree/master/src/Polly.Shared/CircuitBreaker/ICircuitBreakerPolicy.cs) defines + `CircuitState CircuitState` @@ -961,7 +1022,7 @@ with `ICircuitBreakerPolicy : ICircuitBreakerPolicy` adding: + `TResult LastHandledResult`. -This allows collections of similar kinds of policy to be treated as one - for example, for monitoring all your circuit-breakers as [described here](https://github.com/App-vNext/Polly/pull/205). +This allows collections of similar kinds of policy to be treated as one - for example, for monitoring all your circuit-breakers as [described here](https://github.com/App-vNext/Polly/pull/205). For more detail see: [Polly and interfaces](https://github.com/App-vNext/Polly/wiki/Polly-and-interfaces) on wiki. @@ -983,7 +1044,7 @@ For more info see our blog series: + [Part III: Authoring a reactive custom policy](http://www.thepollyproject.org/2019/02/13/authoring-a-reactive-polly-policy-custom-policies-part-iii-2/) (a policy which react to faults). + [Part IV: Custom policies for all execution types](http://www.thepollyproject.org/2019/02/13/custom-policies-for-all-execution-types-custom-policies-part-iv/): sync and async, generic and non-generic. -We provide a [starter template for a custom policy](https://github.com/Polly-Contrib/Polly.Contrib.CustomPolicyTemplates) for developing your own custom policy. +We provide a [starter template for a custom policy](https://github.com/Polly-Contrib/Polly.Contrib.CustomPolicyTemplates) for developing your own custom policy. # Polly-Contrib @@ -996,7 +1057,7 @@ We also provide: + a blank [starter template for a custom policy](https://github.com/Polly-Contrib/Polly.Contrib.CustomPolicyTemplates) (see above for more on custom policies) + a [template repo for any other contrib](https://github.com/Polly-Contrib/Polly.Contrib.BlankTemplate) -Both templates contain a full project structure referencing Polly, Polly's default build targets, and a build to build and test your contrib and make a nuget package. +Both templates contain a full project structure referencing Polly, Polly's default build targets, and a build to build and test your contrib and make a NuGet package. ## Available via Polly-Contrib @@ -1030,7 +1091,7 @@ Both templates contain a full project structure referencing Polly, Polly's defau * [@reisenberger](https://github.com/reisenberger) - Added async support for ContextualPolicy * [@reisenberger](https://github.com/reisenberger) - Added ContextualPolicy support for circuit-breaker * [@reisenberger](https://github.com/reisenberger) - Extended circuit-breaker for public monitoring and control -* [@reisenberger](https://github.com/reisenberger) - Added ExecuteAndCapture support with arbitrary context data +* [@reisenberger](https://github.com/reisenberger) - Added ExecuteAndCapture support with arbitrary context data * [@kristianhald](https://github.com/kristianhald) and [@reisenberger](https://github.com/reisenberger) - Added AdvancedCircuitBreaker * [@reisenberger](https://github.com/reisenberger) - Allowed async onRetry delegates to async retry policies * [@Lumirris](https://github.com/Lumirris) - Add new Polly.Net40Async project/package supporting async for .NET40 via Microsoft.Bcl.Async @@ -1038,11 +1099,11 @@ Both templates contain a full project structure referencing Polly, Polly's defau * [@reisenberger](https://github.com/reisenberger) - Allowed policies to handle returned results; added strongly-typed policies Policy<TResult>;. * [@christopherbahr](https://github.com/christopherbahr) - Added optimisation for circuit-breaker hot path. * [@Finity](https://github.com/Finity) - Fixed circuit-breaker threshold bug. -* [@reisenberger](https://github.com/reisenberger) - Add some missing ExecuteAndCapture/Async overloads. +* [@reisenberger](https://github.com/reisenberger) - Add some missing ExecuteAndCapture/Async overloads. * [@brunolauze](https://github.com/brunolauze) - Add CancellationToken support to synchronous executions (to support TimeoutPolicy). * [@reisenberger](https://github.com/reisenberger) - Add PolicyWrap. -* [@reisenberger](https://github.com/reisenberger) - Add Fallback policy. -* [@reisenberger](https://github.com/reisenberger) - Add PolicyKeys and context to all policy executions, as bedrock for policy events and metrics tracking executions. +* [@reisenberger](https://github.com/reisenberger) - Add Fallback policy. +* [@reisenberger](https://github.com/reisenberger) - Add PolicyKeys and context to all policy executions, as bedrock for policy events and metrics tracking executions. * [@reisenberger](https://github.com/reisenberger), and contributions from [@brunolauze](https://github.com/brunolauze) - Add Bulkhead Isolation policy. * [@reisenberger](https://github.com/reisenberger) - Add Timeout policy. * [@reisenberger](https://github.com/reisenberger) - Fix .NETStandard 1.0 targeting. Remove PCL259 target. PCL259 support is provided via .NETStandard1.0 target, going forward. @@ -1059,12 +1120,12 @@ Both templates contain a full project structure referencing Polly, Polly's defau * [@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) +* 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. +* [@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. * [@awarrenlove](https://github.com/awarrenlove) - Add ability to calculate cache Ttl based on item to cache. * [@erickhouse](https://github.com/erickhouse) - Add a new onBreak overload that provides the prior state on a transition to an open state. @@ -1085,6 +1146,7 @@ Both templates contain a full project structure referencing Polly, Polly's defau * [@jnyrup](https://github.com/jnyrup) - Upgrade tests to Fluent Assertions v5.9.0 * [@SimonCropp](https://github.com/SimonCropp) - Add netcoreapp3.0 target; code clean-ups. * [@aerotog](https://github.com/aerotog) and [@reisenberger](https://github.com/reisenberger) - IConcurrentPolicyRegistry methods on PolicyRegistry +* [@reisenberger](https://github.com/reisenberger) and [@martincostello](https://github.com/martincostello) - Add RateLimit policy. # Sample Projects @@ -1096,9 +1158,9 @@ Both templates contain a full project structure referencing Polly, Polly's defau Please be sure to branch from the head of the latest vX.Y.Z dev branch (rather than master) when developing contributions. -For github workflow, check out our [Wiki](https://github.com/App-vNext/Polly/wiki/Git-Workflow). We are following the excellent GitHub Flow process, and would like to make sure you have all of the information needed to be a world-class contributor! +For GitHub workflow, check out our [Wiki](https://github.com/App-vNext/Polly/wiki/Git-Workflow). We are following the excellent GitHub Flow process, and would like to make sure you have all of the information needed to be a world-class contributor! -Since Polly is part of the .NET Foundation, we ask our contributors to abide by their [Code of Conduct](https://www.dotnetfoundation.org/code-of-conduct). To contribute (beyond trivial typo corrections), review and sign the [.Net Foundation Contributor License Agreement](https://cla.dotnetfoundation.org/). This ensures the community is free to use your contributions. The registration process can be completed entirely online. +Since Polly is part of the .NET Foundation, we ask our contributors to abide by their [Code of Conduct](https://www.dotnetfoundation.org/code-of-conduct). To contribute (beyond trivial typo corrections), review and sign the [.NET Foundation Contributor License Agreement](https://cla.dotnetfoundation.org/). This ensures the community is free to use your contributions. The registration process can be completed entirely online. Also, we've stood up a [Slack](http://www.pollytalk.org) channel for easier real-time discussion of ideas and the general direction of Polly as a whole. Be sure to [join the conversation](http://www.pollytalk.org) today! @@ -1106,14 +1168,14 @@ Also, we've stood up a [Slack](http://www.pollytalk.org) channel for easier real Licensed under the terms of the [New BSD License](http://opensource.org/licenses/BSD-3-Clause) -# Blogs, podcasts, courses, ebooks, architecture samples and videos around Polly +# Blogs, podcasts, courses, e-books, architecture samples and videos around Polly When we discover an interesting write-up on Polly, we'll add it to this list. If you have a blog post you'd like to share, please submit a PR! ## Blog posts * [Try .NET Samples of Polly, the .NET Resilience Framework](https://github.com/bryanjhogan/trydotnet-polly) - by [Bryan Hogan](https://nodogmablog.bryanhogan.net/) * [Create exceptional interactive documentation with Try .NET - The Polly NuGet library did!](https://www.hanselman.com/blog/CreateExceptionalInteractiveDocumentationWithTryNETThePollyNuGetLibraryDid.aspx) - by [Scott Hanselman](https://www.hanselman.com/about/) (writing about the work of Bryan Hogan) -* [Adding resilience and Transient Fault handling to your .NET Core HttpClient with Polly](https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx) - by [Scott Hanselman](https://www.hanselman.com/about/) +* [Adding resilience and Transient Fault handling to your .NET Core HttpClient with Polly](https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx) - by [Scott Hanselman](https://www.hanselman.com/about/) * [Reliable Event Processing in Azure Functions](https://hackernoon.com/reliable-event-processing-in-azure-functions-37054dc2d0fc) - by [Jeff Hollan](https://hackernoon.com/@jeffhollan) * [Optimally configuring ASPNET Core HttpClientFactory](https://rehansaeed.com/optimally-configuring-asp-net-core-httpclientfactory/) including with Polly policies - by [Muhammad Rehan Saeed](https://twitter.com/RehanSaeedUK/) * [Integrating HttpClientFactory with Polly for transient fault handling](https://www.stevejgordon.co.uk/httpclientfactory-using-polly-for-transient-fault-handling) - by [Steve Gordon](https://www.stevejgordon.co.uk/) @@ -1144,15 +1206,15 @@ When we discover an interesting write-up on Polly, we'll add it to this list. If * [Bryan Hogan](https://twitter.com/bryanjhogan) of the [NoDogmaBlog](http://nodogmablog.bryanhogan.net/) has authored a [PluralSight course on Polly](https://www.pluralsight.com/courses/polly-fault-tolerant-web-service-requests). The course takes you through all the major features of Polly, with an additional module added in the fall of 2018 on Http Client Factory. The course examples are based around using Polly for fault tolerance when calling remote web services, but the principles and techniques are applicable to any context in which Polly may be used. -## Sample microservices architecture and ebook +## Sample microservices architecture and e-book ### Sample microservices architecture * [Cesar de la Torre](https://github.com/CESARDELATORRE) produced the Microsoft [eShopOnContainers project](https://github.com/dotnet-architecture/eShopOnContainers), a sample project demonstrating a .NET Microservices architecture. The project uses Polly retry and circuit-breaker policies for resilience in calls to microservices, and in establishing connections to transports such as RabbitMQ. -### ebook +### e-book -* Accompanying the project is a [.Net Microservices Architecture ebook](https://www.microsoft.com/net/download/thank-you/microservices-architecture-ebook) with an extensive section (section 8) on using Polly for resilience, to which [Dylan Reisenberger](https://twitter.com/softwarereisen) contributed. The ebook and code is now (June 2018) updated for using the latest ASP NET Core 2.1 features, [Polly with IHttpClientFactory](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory). +* Accompanying the project is a [.NET Microservices Architecture ebook](https://www.microsoft.com/net/download/thank-you/microservices-architecture-ebook) with an extensive section (section 8) on using Polly for resilience, to which [Dylan Reisenberger](https://twitter.com/softwarereisen) contributed. The e-book and code is now (June 2018) updated for using the latest ASP NET Core 2.1 features, [Polly with IHttpClientFactory](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory). ## Twitter @@ -1161,7 +1223,7 @@ When we discover an interesting write-up on Polly, we'll add it to this list. If ## Videos * [Robust Applications with Polly, the .NET Resilience Framework](https://www.infoq.com/presentations/polly), Bryan Hogan introduces Polly and explains how to use it to build a fault tolerant application. -* From MVP [Houssem Dellai](https://github.com/HoussemDellai), a [youtube video on How to use Polly with Xamarin Apps](https://www.youtube.com/watch?v=7vsN0RkFN_E), covering wait-and-retry and discussing circuit-breaker policy with a demonstration in Xamarin Forms. Here is the [source code](https://github.com/HoussemDellai/ResilientHttpClient) of the application demonstrated in the video. Draws on the [`ResilientHttpClient`](https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs) from Microsoft's [eShopOnContainers project](https://github.com/dotnet-architecture/eShopOnContainers). +* From MVP [Houssem Dellai](https://github.com/HoussemDellai), a [YouTube video on How to use Polly with Xamarin Apps](https://www.youtube.com/watch?v=7vsN0RkFN_E), covering wait-and-retry and discussing circuit-breaker policy with a demonstration in Xamarin Forms. Here is the [source code](https://github.com/HoussemDellai/ResilientHttpClient) of the application demonstrated in the video. Draws on the [`ResilientHttpClient`](https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/BuildingBlocks/Resilience/Resilience.Http/ResilientHttpClient.cs) from Microsoft's [eShopOnContainers project](https://github.com/dotnet-architecture/eShopOnContainers). * In the video, [.NET Rocks Live with Jon Skeet and Bill Wagner](https://youtu.be/LCj7h7ZoHA8?t=1617), Bill Wagner discusses Polly. * Scott Allen discusses Polly during his [Building for Resiliency and Scale in the Cloud](https://youtu.be/SFLu6jZWXGs?t=1440) presentation at NDC. -* [ASP.NET Community Standup April 24, 2018](https://youtu.be/k0Xy-5zE9to?t=12m22s): Damian Edwards, Jon Galloway and Scott Hanselman discuss Scott Hanselman's blog on [Polly with IHttpClientFactory](https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx) and the [Polly team documentation on IHttpClientFactory](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory). Interesting background discussion also on feature richness and the importance of good documentation. +* [ASP.NET Community Standup April 24, 2018](https://youtu.be/k0Xy-5zE9to?t=12m22s): Damian Edwards, Jon Galloway and Scott Hanselman discuss Scott Hanselman's blog on [Polly with IHttpClientFactory](https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx) and the [Polly team documentation on IHttpClientFactory](https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory). Interesting background discussion also on feature richness and the importance of good documentation. diff --git a/build.cake b/build.cake index 7d335a7c7c4..a18ca7ce761 100644 --- a/build.cake +++ b/build.cake @@ -16,8 +16,8 @@ var configuration = Argument("configuration", "Release"); // EXTERNAL NUGET LIBRARIES ////////////////////////////////////////////////////////////////////// -#addin "Cake.FileHelpers" -#addin nuget:?package=Cake.Yaml +#addin nuget:?package=Cake.FileHelpers&version=3.3.0 +#addin nuget:?package=Cake.Yaml&version=3.1.1 #addin nuget:?package=YamlDotNet&version=5.2.1 /////////////////////////////////////////////////////////////////////////////// diff --git a/build.ps1 b/build.ps1 index 8aa2a8de321..5d095556f4d 100644 --- a/build.ps1 +++ b/build.ps1 @@ -111,7 +111,7 @@ if(-Not $SkipToolPackageRestore.IsPresent) # Install just Cake if missing config else { - $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install Cake -Version 0.25.0 -ExcludeVersion" # Pin Cake version to 0.25.0; see https://github.com/App-vNext/Polly/issues/416 + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install Cake -Version 0.38.5 -ExcludeVersion" # Pin Cake version to 0.38.5; see https://github.com/App-vNext/Polly/issues/416 Write-Verbose ($NuGetOutput | Out-String) } Pop-Location diff --git a/src/Polly.Benchmarks/Polly.Benchmarks.csproj b/src/Polly.Benchmarks/Polly.Benchmarks.csproj index 165dfe8860b..1c0315cb01a 100644 --- a/src/Polly.Benchmarks/Polly.Benchmarks.csproj +++ b/src/Polly.Benchmarks/Polly.Benchmarks.csproj @@ -1,5 +1,6 @@  + false latest Exe net5.0 diff --git a/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs b/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs new file mode 100644 index 00000000000..64da0965670 --- /dev/null +++ b/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs @@ -0,0 +1,40 @@ +using System; +using FluentAssertions; +using Polly.RateLimit; + +namespace Polly.Specs.Helpers.RateLimit +{ + internal static class IRateLimiterExtensions + { + public static void ShouldPermitAnExecution(this IRateLimiter rateLimiter) + { + (bool permitExecution, TimeSpan retryAfter) canExecute = rateLimiter.PermitExecution(); + + canExecute.permitExecution.Should().BeTrue(); + canExecute.retryAfter.Should().Be(TimeSpan.Zero); + } + + public static void ShouldPermitNExecutions(this IRateLimiter rateLimiter, long numberOfExecutions) + { + for (int execution = 0; execution < numberOfExecutions; execution++) + { + rateLimiter.ShouldPermitAnExecution(); + } + } + + public static void ShouldNotPermitAnExecution(this IRateLimiter rateLimiter, TimeSpan? retryAfter = null) + { + (bool permitExecution, TimeSpan retryAfter) canExecute = rateLimiter.PermitExecution(); + + canExecute.permitExecution.Should().BeFalse(); + if (retryAfter == null) + { + canExecute.retryAfter.Should().BeGreaterThan(TimeSpan.Zero); + } + else + { + canExecute.retryAfter.Should().Be(retryAfter.Value); + } + } + } +} \ No newline at end of file diff --git a/src/Polly.Specs/Helpers/RateLimit/ResultClassWithRetryAfter.cs b/src/Polly.Specs/Helpers/RateLimit/ResultClassWithRetryAfter.cs new file mode 100644 index 00000000000..fcb1c72c46d --- /dev/null +++ b/src/Polly.Specs/Helpers/RateLimit/ResultClassWithRetryAfter.cs @@ -0,0 +1,21 @@ +using System; + +namespace Polly.Specs.Helpers.RateLimit +{ + internal class ResultClassWithRetryAfter : ResultClass + { + public TimeSpan RetryAfter { get; } + + public ResultClassWithRetryAfter(ResultPrimitive result) + : base(result) + { + RetryAfter = TimeSpan.Zero; + } + + public ResultClassWithRetryAfter(TimeSpan retryAfter) + : base(ResultPrimitive.Undefined) + { + RetryAfter = retryAfter; + } + } +} diff --git a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs new file mode 100644 index 00000000000..24b70fa2e50 --- /dev/null +++ b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Polly.RateLimit; +using Polly.Specs.Helpers; +using Polly.Specs.Helpers.RateLimit; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + public class AsyncRateLimitPolicySpecs : RateLimitPolicySpecsBase, IDisposable + { + public void Dispose() + { + SystemClock.Reset(); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) + { + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) + { + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst); + } + + protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy policy) + { + if (policy is AsyncRateLimitPolicy typedPolicy) + { + try + { + typedPolicy.ExecuteAsync(() => Task.FromResult(new ResultClassWithRetryAfter(ResultPrimitive.Good))).GetAwaiter().GetResult(); + return (true, TimeSpan.Zero); + } + catch (RateLimitRejectedException e) + { + return (false, e.RetryAfter); + } + } + else + { + throw new InvalidOperationException("Unexpected policy type in test construction."); + } + } + } +} diff --git a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs new file mode 100644 index 00000000000..19073d3f3ed --- /dev/null +++ b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Polly.RateLimit; +using Polly.Specs.Helpers; +using Polly.Specs.Helpers.RateLimit; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + public class AsyncRateLimitPolicyTResultSpecs : RateLimitPolicyTResultSpecsBase, IDisposable + { + public void Dispose() + { + SystemClock.Reset(); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) + { + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) + { + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst, + Func retryAfterFactory) + { + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst, retryAfterFactory); + } + + protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy policy) + { + if (policy is AsyncRateLimitPolicy typedPolicy) + { + try + { + typedPolicy.ExecuteAsync(() => Task.FromResult(new ResultClassWithRetryAfter(ResultPrimitive.Good))).GetAwaiter().GetResult(); + return (true, TimeSpan.Zero); + } + catch (RateLimitRejectedException e) + { + return (false, e.RetryAfter); + } + } + else + { + throw new InvalidOperationException("Unexpected policy type in test construction."); + } + } + + protected override TResult TryExecuteThroughPolicy(IRateLimitPolicy policy, Context context, TResult resultIfExecutionPermitted) + { + if (policy is AsyncRateLimitPolicy typedPolicy) + { + return typedPolicy.ExecuteAsync(ctx => Task.FromResult(resultIfExecutionPermitted), context).GetAwaiter().GetResult(); + } + else + { + throw new InvalidOperationException("Unexpected policy type in test construction."); + } + } + } +} diff --git a/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs new file mode 100644 index 00000000000..31376594f63 --- /dev/null +++ b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs @@ -0,0 +1,11 @@ +using System; +using Polly.RateLimit; + +namespace Polly.Specs.RateLimit +{ + public class LockFreeTokenBucketRateLimiterTests : TokenBucketRateLimiterTestsBase + { + internal override IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) + => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); + } +} diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs b/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs new file mode 100644 index 00000000000..dec7aa35f0d --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs @@ -0,0 +1,48 @@ +using System; +using Polly.RateLimit; +using Polly.Specs.Helpers; +using Polly.Specs.Helpers.RateLimit; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + public class RateLimitPolicySpecs : RateLimitPolicySpecsBase, IDisposable + { + public void Dispose() + { + SystemClock.Reset(); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) + { + return Policy.RateLimit(numberOfExecutions, perTimeSpan); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) + { + return Policy.RateLimit(numberOfExecutions, perTimeSpan, maxBurst); + } + + protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy policy) + { + if (policy is RateLimitPolicy typedPolicy) + { + try + { + typedPolicy.Execute(() => new ResultClassWithRetryAfter(ResultPrimitive.Good)); + return (true, TimeSpan.Zero); + } + catch (RateLimitRejectedException e) + { + return (false, e.RetryAfter); + } + } + else + { + throw new InvalidOperationException("Unexpected policy type in test construction."); + } + } + } +} \ No newline at end of file diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs new file mode 100644 index 00000000000..9d39e15fde2 --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs @@ -0,0 +1,296 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Polly.RateLimit; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + public abstract class RateLimitPolicySpecsBase : RateLimitSpecsBase + { + protected abstract IRateLimitPolicy GetPolicyViaSyntax( + int numberOfExecutions, + TimeSpan perTimeSpan); + + protected abstract IRateLimitPolicy GetPolicyViaSyntax( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst); + + protected abstract (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy policy); + + protected void ShouldPermitAnExecution(IRateLimitPolicy policy) + { + (bool permitExecution, TimeSpan retryAfter) = TryExecuteThroughPolicy(policy); + + permitExecution.Should().BeTrue(); + retryAfter.Should().Be(TimeSpan.Zero); + } + + protected void ShouldPermitNExecutions(IRateLimitPolicy policy, long numberOfExecutions) + { + for (int execution = 0; execution < numberOfExecutions; execution++) + { + ShouldPermitAnExecution(policy); + } + } + + protected void ShouldNotPermitAnExecution(IRateLimitPolicy policy, TimeSpan? retryAfter = null) + { + (bool permitExecution, TimeSpan retryAfter) canExecute = TryExecuteThroughPolicy(policy); + + canExecute.permitExecution.Should().BeFalse(); + if (retryAfter == null) + { + canExecute.retryAfter.Should().BeGreaterThan(TimeSpan.Zero); + } + else + { + canExecute.retryAfter.Should().Be(retryAfter.Value); + } + } + + [Fact] + public void Syntax_should_throw_for_perTimeSpan_zero() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.Zero); + + invalidSyntax.Should().Throw().And.ParamName.Should().Be("perTimeSpan"); + } + + [Fact] + public void Syntax_should_throw_for_numberOfExecutions_negative() + { + Action invalidSyntax = () => GetPolicyViaSyntax(-1, TimeSpan.FromSeconds(1)); + + invalidSyntax.Should().Throw().And.ParamName.Should().Be("numberOfExecutions"); + } + + [Fact] + public void Syntax_should_throw_for_numberOfExecutions_zero() + { + Action invalidSyntax = () => GetPolicyViaSyntax(0, TimeSpan.FromSeconds(1)); + + invalidSyntax.Should().Throw().And.ParamName.Should().Be("numberOfExecutions"); + } + + [Fact] + public void Syntax_should_throw_for_perTimeSpan_negative() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromTicks(-1)); + + invalidSyntax.Should().Throw().And.ParamName.Should().Be("perTimeSpan"); + } + + [Fact] + public void Syntax_should_throw_for_maxBurst_negative() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromSeconds(1), -1); + + invalidSyntax.Should().Throw().And.ParamName.Should().Be("maxBurst"); + } + + [Fact] + public void Syntax_should_throw_for_maxBurst_zero() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromSeconds(1), 0); + + invalidSyntax.Should().Throw().And.ParamName.Should().Be("maxBurst"); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + public void Given_bucket_capacity_one_and_time_not_advanced_ratelimiter_specifies_correct_wait_until_next_execution(int onePerSeconds) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetPolicyViaSyntax(1, onePer); + + // Assert - first execution after initialising should always be permitted. + ShouldPermitAnExecution(rateLimiter); + + // Arrange + // (do nothing - time not advanced) + + // Assert - should be blocked - time not advanced. + ShouldNotPermitAnExecution(rateLimiter, onePer); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(50)] + public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_executions_up_to_bucket_capacity(int bucketCapacity) + { + FixClock(); + + // Arrange. + TimeSpan onePer = TimeSpan.FromSeconds(1); + var rateLimiter = GetPolicyViaSyntax(1, onePer, bucketCapacity); + + // Act - should be able to successfully take bucketCapacity items. + ShouldPermitNExecutions(rateLimiter, bucketCapacity); + + // Assert - should not be able to take any items (given time not advanced). + ShouldNotPermitAnExecution(rateLimiter, onePer); + } + + [Theory] + [InlineData(1, 1)] + [InlineData(2, 1)] + [InlineData(5, 1)] + [InlineData(1, 10)] + [InlineData(2, 10)] + [InlineData(5, 10)] + public void Given_any_bucket_capacity_ratelimiter_permits_another_execution_per_interval(int onePerSeconds, int bucketCapacity) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetPolicyViaSyntax(1, onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + ShouldPermitNExecutions(rateLimiter, bucketCapacity); + ShouldNotPermitAnExecution(rateLimiter); + + // Act-Assert - repeatedly advance the clock towards the interval but not quite - then to the interval + int experimentRepeats = bucketCapacity * 3; + TimeSpan shortfallFromInterval = TimeSpan.FromTicks(1); + TimeSpan notQuiteInterval = onePer - shortfallFromInterval; + for (int i = 0; i < experimentRepeats; i++) + { + // Arrange - Advance clock not quite to the interval + AdvanceClock(notQuiteInterval.Ticks); + + // Assert - should not quite be able to issue another token + ShouldNotPermitAnExecution(rateLimiter, shortfallFromInterval); + + // Arrange - Advance clock to the interval + AdvanceClock(shortfallFromInterval.Ticks); + + // Act + ShouldPermitAnExecution(rateLimiter); + + // Assert - but cannot get another token straight away + ShouldNotPermitAnExecution(rateLimiter); + } + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + public void Given_any_bucket_capacity_rate_limiter_permits_full_bucket_burst_after_exact_elapsed_time(int bucketCapacity) + { + FixClock(); + + // Arrange + int onePerSeconds = 1; + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetPolicyViaSyntax(1, onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + ShouldPermitNExecutions(rateLimiter, bucketCapacity); + ShouldNotPermitAnExecution(rateLimiter); + + // Arrange - advance exactly enough to permit a full bucket burst + AdvanceClock(onePer.Ticks * bucketCapacity); + + // Assert - expect full bucket capacity but no more + ShouldPermitNExecutions(rateLimiter, bucketCapacity); + ShouldNotPermitAnExecution(rateLimiter); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + public void Given_any_bucket_capacity_rate_limiter_permits_half_full_bucket_burst_after_half_required_refill_time_elapsed(int bucketCapacity) + { + (bucketCapacity % 2).Should().Be(0); + + FixClock(); + + // Arrange + int onePerSeconds = 1; + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetPolicyViaSyntax(1, onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + ShouldPermitNExecutions(rateLimiter, bucketCapacity); + ShouldNotPermitAnExecution(rateLimiter); + + // Arrange - advance multiple times enough to permit a full bucket burst + AdvanceClock(onePer.Ticks * (bucketCapacity / 2)); + + // Assert - expect full bucket capacity but no more + ShouldPermitNExecutions(rateLimiter, bucketCapacity / 2); + ShouldNotPermitAnExecution(rateLimiter); + } + + [Theory] + [InlineData(100, 2)] + [InlineData(100, 5)] + public void Given_any_bucket_capacity_rate_limiter_permits_only_full_bucket_burst_even_if_multiple_required_refill_time_elapsed(int bucketCapacity, int multipleRefillTimePassed) + { + multipleRefillTimePassed.Should().BeGreaterThan(1); + + FixClock(); + + // Arrange + int onePerSeconds = 1; + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetPolicyViaSyntax(1, onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + ShouldPermitNExecutions(rateLimiter, bucketCapacity); + ShouldNotPermitAnExecution(rateLimiter); + + // Arrange - advance multiple times enough to permit a full bucket burst + AdvanceClock(onePer.Ticks * bucketCapacity * multipleRefillTimePassed); + + // Assert - expect full bucket capacity but no more + ShouldPermitNExecutions(rateLimiter, bucketCapacity); + ShouldNotPermitAnExecution(rateLimiter); + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + [InlineData(100)] + public void Given_immediate_parallel_contention_ratelimiter_still_only_permits_one(int parallelContention) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(1); + var rateLimiter = GetPolicyViaSyntax(1, onePer); + + // Arrange - parallel tasks all waiting on a manual reset event. + ManualResetEventSlim gate = new(); + Task<(bool permitExecution, TimeSpan retryAfter)>[] tasks = new Task<(bool, TimeSpan)>[parallelContention]; + for (int i = 0; i < parallelContention; i++) + { + tasks[i] = Task.Run(() => + { + gate.Wait(); + return TryExecuteThroughPolicy(rateLimiter); + }); + } + + // Act - release gate. + gate.Set(); + Within(TimeSpan.FromSeconds(10 /* high to allow for slow-running on time-slicing CI servers */), () => tasks.All(t => t.IsCompleted).Should().BeTrue()); + + // Assert - one should have permitted execution, n-1 not. + var results = tasks.Select(t => t.Result).ToList(); + results.Count(r => r.permitExecution).Should().Be(1); + results.Count(r => !r.permitExecution).Should().Be(parallelContention - 1); + } + } +} diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs new file mode 100644 index 00000000000..e67df0c2fca --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs @@ -0,0 +1,66 @@ +using System; +using Polly.RateLimit; +using Polly.Specs.Helpers; +using Polly.Specs.Helpers.RateLimit; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + public class RateLimitPolicyTResultSpecs : RateLimitPolicyTResultSpecsBase, IDisposable + { + public void Dispose() + { + SystemClock.Reset(); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) + { + return Policy.RateLimit(numberOfExecutions, perTimeSpan); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) + { + return Policy.RateLimit(numberOfExecutions, perTimeSpan, maxBurst); + } + + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst, + Func retryAfterFactory) + { + return Policy.RateLimit(numberOfExecutions, perTimeSpan, maxBurst, retryAfterFactory); + } + + protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy policy) + { + if (policy is RateLimitPolicy typedPolicy) + { + try + { + typedPolicy.Execute(() => new ResultClassWithRetryAfter(ResultPrimitive.Good)); + return (true, TimeSpan.Zero); + } + catch (RateLimitRejectedException e) + { + return (false, e.RetryAfter); + } + } + else + { + throw new InvalidOperationException("Unexpected policy type in test construction."); + } + } + + protected override TResult TryExecuteThroughPolicy(IRateLimitPolicy policy, Context context, TResult resultIfExecutionPermitted) + { + if (policy is RateLimitPolicy typedPolicy) + { + return typedPolicy.Execute(ctx => resultIfExecutionPermitted, context); + } + else + { + throw new InvalidOperationException("Unexpected policy type in test construction."); + } + } + } +} diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs new file mode 100644 index 00000000000..3fc16eb21ef --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs @@ -0,0 +1,57 @@ +using System; +using FluentAssertions; +using Polly.RateLimit; +using Polly.Specs.Helpers; +using Polly.Specs.Helpers.RateLimit; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + public abstract class RateLimitPolicyTResultSpecsBase : RateLimitPolicySpecsBase + { + protected abstract IRateLimitPolicy GetPolicyViaSyntax( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst, + Func retryAfterFactory); + + protected abstract TResult TryExecuteThroughPolicy(IRateLimitPolicy policy, Context context, TResult resultIfExecutionPermitted); + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + public void Ratelimiter_specifies_correct_wait_until_next_execution_by_custom_factory_passing_correct_context(int onePerSeconds) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + Context contextPassedToRetryAfter = null; + Func retryAfterFactory = (t, ctx) => + { + contextPassedToRetryAfter = ctx; + return new ResultClassWithRetryAfter(t); + }; + var rateLimiter = GetPolicyViaSyntax(1, onePer, 1, retryAfterFactory); + + // Arrange - drain first permitted execution after initialising. + ShouldPermitAnExecution(rateLimiter); + + // Arrange + // (do nothing - time not advanced) + + // Act - try another execution. + Context contextToPassIn = new Context(); + var resultExpectedBlocked = TryExecuteThroughPolicy(rateLimiter, contextToPassIn, new ResultClassWithRetryAfter(ResultPrimitive.Good)); + + // Assert - should be blocked - time not advanced. + resultExpectedBlocked.ResultCode.Should().NotBe(ResultPrimitive.Good); + // Result should be expressed per the retryAfterFactory. + resultExpectedBlocked.RetryAfter.Should().Be(onePer); + // Context should have been passed to the retryAfterFactory. + contextPassedToRetryAfter.Should().NotBeNull(); + contextPassedToRetryAfter.Should().BeSameAs(contextToPassIn); + } + } +} diff --git a/src/Polly.Specs/RateLimit/RateLimitSpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitSpecsBase.cs new file mode 100644 index 00000000000..b5bcd58360a --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitSpecsBase.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; +using System.Threading; +using FluentAssertions.Execution; +using Polly.Utilities; +using Xunit.Sdk; + +namespace Polly.Specs.RateLimit +{ + public abstract class RateLimitSpecsBase + { + /// + /// Asserts that the actionContainingAssertions will succeed without or , within the given timespan. Checks are made each time a status-change pulse is received from the s executing through the bulkhead. + /// + /// The allowable timespan. + /// The action containing fluent assertions, which must succeed within the timespan. + protected void Within(TimeSpan timeSpan, Action actionContainingAssertions) + { + TimeSpan retryInterval = TimeSpan.FromSeconds(0.2); + + Stopwatch watch = Stopwatch.StartNew(); + while (true) + { + try + { + actionContainingAssertions.Invoke(); + break; + } + catch (Exception e) + { + if (!(e is AssertionFailedException || e is XunitException)) { throw; } + + if (watch.Elapsed > timeSpan) { throw; } + + Thread.Sleep(retryInterval); + } + } + } + + protected static void FixClock() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + SystemClock.DateTimeOffsetUtcNow = () => now; + } + + protected static void AdvanceClock(TimeSpan advance) + { + DateTimeOffset now = SystemClock.DateTimeOffsetUtcNow(); + SystemClock.DateTimeOffsetUtcNow = () => now + advance; + } + + protected static void AdvanceClock(long advanceTicks) => AdvanceClock(TimeSpan.FromTicks(advanceTicks)); + } +} diff --git a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs new file mode 100644 index 00000000000..c296f0d5bf7 --- /dev/null +++ b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs @@ -0,0 +1,216 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Polly.RateLimit; +using Polly.Specs.Helpers.RateLimit; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + public abstract class TokenBucketRateLimiterTestsBase : RateLimitSpecsBase, IDisposable + { + internal abstract IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity); + + public void Dispose() + { + SystemClock.Reset(); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + public void Given_bucket_capacity_one_and_time_not_advanced_ratelimiter_specifies_correct_wait_until_next_execution(int onePerSeconds) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetRateLimiter(onePer, 1); + + // Assert - first execution after initialising should always be permitted. + rateLimiter.ShouldPermitAnExecution(); + + // Arrange + // (do nothing - time not advanced) + + // Assert - should be blocked - time not advanced. + rateLimiter.ShouldNotPermitAnExecution(onePer); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(50)] + public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_executions_up_to_bucket_capacity(int bucketCapacity) + { + FixClock(); + + // Arrange. + TimeSpan onePer = TimeSpan.FromSeconds(1); + var rateLimiter = GetRateLimiter(onePer, bucketCapacity); + + // Act - should be able to successfully take bucketCapacity items. + rateLimiter.ShouldPermitNExecutions(bucketCapacity); + + // Assert - should not be able to take any items (given time not advanced). + rateLimiter.ShouldNotPermitAnExecution(onePer); + } + + [Theory] + [InlineData(1, 1)] + [InlineData(2, 1)] + [InlineData(5, 1)] + [InlineData(1, 10)] + [InlineData(2, 10)] + [InlineData(5, 10)] + public void Given_any_bucket_capacity_ratelimiter_permits_another_execution_per_interval(int onePerSeconds, int bucketCapacity) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetRateLimiter(onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + rateLimiter.ShouldPermitNExecutions(bucketCapacity); + rateLimiter.ShouldNotPermitAnExecution(); + + // Act-Assert - repeatedly advance the clock towards the interval but not quite - then to the interval + int experimentRepeats = bucketCapacity * 3; + TimeSpan shortfallFromInterval = TimeSpan.FromTicks(1); + TimeSpan notQuiteInterval = onePer - shortfallFromInterval; + for (int i = 0; i < experimentRepeats; i++) + { + // Arrange - Advance clock not quite to the interval + AdvanceClock(notQuiteInterval.Ticks); + + // Assert - should not quite be able to issue another token + rateLimiter.ShouldNotPermitAnExecution(shortfallFromInterval); + + // Arrange - Advance clock to the interval + AdvanceClock(shortfallFromInterval.Ticks); + + // Act + rateLimiter.ShouldPermitAnExecution(); + + // Assert - but cannot get another token straight away + rateLimiter.ShouldNotPermitAnExecution(); + } + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + public void Given_any_bucket_capacity_rate_limiter_permits_full_bucket_burst_after_exact_elapsed_time(int bucketCapacity) + { + FixClock(); + + // Arrange + int onePerSeconds = 1; + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetRateLimiter(onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + rateLimiter.ShouldPermitNExecutions(bucketCapacity); + rateLimiter.ShouldNotPermitAnExecution(); + + // Arrange - advance exactly enough to permit a full bucket burst + AdvanceClock(onePer.Ticks * bucketCapacity); + + // Assert - expect full bucket capacity but no more + rateLimiter.ShouldPermitNExecutions(bucketCapacity); + rateLimiter.ShouldNotPermitAnExecution(); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + public void Given_any_bucket_capacity_rate_limiter_permits_half_full_bucket_burst_after_half_required_refill_time_elapsed(int bucketCapacity) + { + (bucketCapacity % 2).Should().Be(0); + + FixClock(); + + // Arrange + int onePerSeconds = 1; + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetRateLimiter(onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + rateLimiter.ShouldPermitNExecutions(bucketCapacity); + rateLimiter.ShouldNotPermitAnExecution(); + + // Arrange - advance multiple times enough to permit a full bucket burst + AdvanceClock(onePer.Ticks * (bucketCapacity / 2)); + + // Assert - expect full bucket capacity but no more + rateLimiter.ShouldPermitNExecutions(bucketCapacity / 2); + rateLimiter.ShouldNotPermitAnExecution(); + } + + [Theory] + [InlineData(100, 2)] + [InlineData(100, 5)] + public void Given_any_bucket_capacity_rate_limiter_permits_only_full_bucket_burst_even_if_multiple_required_refill_time_elapsed(int bucketCapacity, int multipleRefillTimePassed) + { + multipleRefillTimePassed.Should().BeGreaterThan(1); + + FixClock(); + + // Arrange + int onePerSeconds = 1; + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetRateLimiter(onePer, bucketCapacity); + + // Arrange - spend the initial bucket capacity. + rateLimiter.ShouldPermitNExecutions(bucketCapacity); + rateLimiter.ShouldNotPermitAnExecution(); + + // Arrange - advance multiple times enough to permit a full bucket burst + AdvanceClock(onePer.Ticks * bucketCapacity * multipleRefillTimePassed); + + // Assert - expect full bucket capacity but no more + rateLimiter.ShouldPermitNExecutions(bucketCapacity); + rateLimiter.ShouldNotPermitAnExecution(); + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + [InlineData(100)] + public void Given_immediate_parallel_contention_ratelimiter_still_only_permits_one(int parallelContention) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(1); + var rateLimiter = GetRateLimiter(onePer, 1); + + // Arrange - parallel tasks all waiting on a manual reset event. + ManualResetEventSlim gate = new ManualResetEventSlim(); + Task<(bool permitExecution, TimeSpan retryAfter)>[] tasks = new Task<(bool, TimeSpan)>[parallelContention]; + for (int i = 0; i < parallelContention; i++) + { + tasks[i] = Task.Run(() => + { + gate.Wait(); + return rateLimiter.PermitExecution(); + }); + } + + // Act - release gate. + gate.Set(); + Within(TimeSpan.FromSeconds(10 /* high to allow for slow-running on time-slicing CI servers */), () => tasks.All(t => t.IsCompleted).Should().BeTrue()); + + // Assert - one should have permitted execution, n-1 not. + var results = tasks.Select(t => t.Result).ToList(); + results.Count(r => r.permitExecution).Should().Be(1); + results.Count(r => !r.permitExecution).Should().Be(parallelContention - 1); + } + } +} diff --git a/src/Polly/AsyncPolicy.ContextAndKeys.cs b/src/Polly/AsyncPolicy.ContextAndKeys.cs index b62edf6e512..97d0c87aa03 100644 --- a/src/Polly/AsyncPolicy.ContextAndKeys.cs +++ b/src/Polly/AsyncPolicy.ContextAndKeys.cs @@ -1,6 +1,4 @@ -using System; - -namespace Polly +namespace Polly { public abstract partial class AsyncPolicy { @@ -9,7 +7,7 @@ public abstract partial class AsyncPolicy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - public AsyncPolicy WithPolicyKey(String policyKey) + public AsyncPolicy WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; @@ -22,7 +20,7 @@ public AsyncPolicy WithPolicyKey(String policyKey) /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - IAsyncPolicy IAsyncPolicy.WithPolicyKey(String policyKey) + IAsyncPolicy IAsyncPolicy.WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; @@ -39,7 +37,7 @@ public abstract partial class AsyncPolicy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - public AsyncPolicy WithPolicyKey(String policyKey) + public AsyncPolicy WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; @@ -52,7 +50,7 @@ public AsyncPolicy WithPolicyKey(String policyKey) /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - IAsyncPolicy IAsyncPolicy.WithPolicyKey(String policyKey) + IAsyncPolicy IAsyncPolicy.WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; diff --git a/src/Polly/Bulkhead/BulkheadRejectedException.cs b/src/Polly/Bulkhead/BulkheadRejectedException.cs index 22d8bbc2ae0..82898dbfdb3 100644 --- a/src/Polly/Bulkhead/BulkheadRejectedException.cs +++ b/src/Polly/Bulkhead/BulkheadRejectedException.cs @@ -24,7 +24,7 @@ public BulkheadRejectedException() : this("The bulkhead semaphore and queue are /// Initializes a new instance of the class. /// /// The message. - public BulkheadRejectedException(String message) : base(message) + public BulkheadRejectedException(string message) : base(message) { } @@ -33,7 +33,7 @@ public BulkheadRejectedException(String message) : base(message) /// /// The message. /// The inner exception. - public BulkheadRejectedException(String message, Exception innerException) : base(message, innerException) + public BulkheadRejectedException(string message, Exception innerException) : base(message, innerException) { } diff --git a/src/Polly/Caching/DefaultCacheKeyStrategy.cs b/src/Polly/Caching/DefaultCacheKeyStrategy.cs index 70350c9caec..28a906c2a86 100644 --- a/src/Polly/Caching/DefaultCacheKeyStrategy.cs +++ b/src/Polly/Caching/DefaultCacheKeyStrategy.cs @@ -1,6 +1,4 @@ -using System; - -namespace Polly.Caching +namespace Polly.Caching { /// /// The default cache key strategy for . Returns the property . @@ -12,7 +10,7 @@ public class DefaultCacheKeyStrategy : ICacheKeyStrategy /// /// The execution context. /// The cache key - public String GetCacheKey(Context context) => context.OperationKey; + public string GetCacheKey(Context context) => context.OperationKey; /// /// Gets an instance of the . diff --git a/src/Polly/Caching/IAsyncCacheProvider.cs b/src/Polly/Caching/IAsyncCacheProvider.cs index 47623adc185..31b4622fe4d 100644 --- a/src/Polly/Caching/IAsyncCacheProvider.cs +++ b/src/Polly/Caching/IAsyncCacheProvider.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Polly.Caching @@ -19,7 +18,7 @@ public interface IAsyncCacheProvider /// A promising as Result a tuple whose first element is a value indicating whether /// the key was found in the cache, and whose second element is the value from the cache (null if not found). /// - Task<(bool, object)> TryGetAsync(String key, CancellationToken cancellationToken, bool continueOnCapturedContext); + Task<(bool, object)> TryGetAsync(string key, CancellationToken cancellationToken, bool continueOnCapturedContext); /// /// Puts the specified value in the cache asynchronously. @@ -48,7 +47,7 @@ public interface IAsyncCacheProvider /// A promising as Result a tuple whose first element is a value indicating whether /// the key was found in the cache, and whose second element is the value from the cache (default(TResult) if not found). /// - Task<(bool, TResult)> TryGetAsync(String key, CancellationToken cancellationToken, bool continueOnCapturedContext); + Task<(bool, TResult)> TryGetAsync(string key, CancellationToken cancellationToken, bool continueOnCapturedContext); /// /// Puts the specified value in the cache asynchronously. diff --git a/src/Polly/Caching/ICacheKeyStrategy.cs b/src/Polly/Caching/ICacheKeyStrategy.cs index 5f06514a647..bea3c94bee3 100644 --- a/src/Polly/Caching/ICacheKeyStrategy.cs +++ b/src/Polly/Caching/ICacheKeyStrategy.cs @@ -1,6 +1,4 @@ -using System; - -namespace Polly.Caching +namespace Polly.Caching { /// /// Defines how a should get a string cache key from an execution @@ -12,6 +10,6 @@ public interface ICacheKeyStrategy /// /// The execution context. /// The cache key - String GetCacheKey(Context context); + string GetCacheKey(Context context); } } diff --git a/src/Polly/Caching/ISyncCacheProvider.cs b/src/Polly/Caching/ISyncCacheProvider.cs index bc6235bad54..15390ee029d 100644 --- a/src/Polly/Caching/ISyncCacheProvider.cs +++ b/src/Polly/Caching/ISyncCacheProvider.cs @@ -1,6 +1,4 @@ -using System; - -namespace Polly.Caching +namespace Polly.Caching { /// /// Defines methods for classes providing synchronous cache functionality for Polly s. @@ -15,7 +13,7 @@ public interface ISyncCacheProvider /// A tuple whose first element is a value indicating whether the key was found in the cache, /// and whose second element is the value from the cache (null if not found). /// - (bool, object) TryGet(String key); + (bool, object) TryGet(string key); /// /// Puts the specified value in the cache. @@ -39,7 +37,7 @@ public interface ISyncCacheProvider /// A tuple whose first element is a value indicating whether the key was found in the cache, /// and whose second element is the value from the cache (default(TResult) if not found). /// - (bool, TResult) TryGet(String key); + (bool, TResult) TryGet(string key); /// /// Puts the specified value in the cache. diff --git a/src/Polly/Context.Dictionary.cs b/src/Polly/Context.Dictionary.cs index 07750752343..12ed0043927 100644 --- a/src/Polly/Context.Dictionary.cs +++ b/src/Polly/Context.Dictionary.cs @@ -22,7 +22,7 @@ public partial class Context : IDictionary, IDictionary, IReadOn /// /// The operation key. /// The context data. - public Context(String operationKey, IDictionary contextData) : this(contextData) + public Context(string operationKey, IDictionary contextData) : this(contextData) { OperationKey = operationKey; } diff --git a/src/Polly/Context.cs b/src/Polly/Context.cs index 9dd3b1244a7..65d0949eca1 100644 --- a/src/Polly/Context.cs +++ b/src/Polly/Context.cs @@ -17,7 +17,7 @@ public partial class Context /// Initializes a new instance of the class, with the specified . /// /// The operation key. - public Context(String operationKey) => OperationKey = operationKey; + public Context(string operationKey) => OperationKey = operationKey; /// /// Initializes a new instance of the class. @@ -29,19 +29,19 @@ public Context() /// /// When execution is through a , identifies the PolicyWrap executing the current delegate by returning the of the outermost layer in the PolicyWrap; otherwise, null. /// - public String PolicyWrapKey { get; internal set; } + public string PolicyWrapKey { get; internal set; } /// /// The of the policy instance executing the current delegate. /// - public String PolicyKey { get; internal set; } + public string PolicyKey { get; internal set; } /// /// A key unique to the call site of the current execution. /// Policy instances are commonly reused across multiple call sites. Set an OperationKey so that logging and metrics can distinguish usages of policy instances at different call sites. /// The value is set by using the constructor taking an operationKey parameter. /// - public String OperationKey { get; } + public string OperationKey { get; } /// /// A Guid guaranteed to be unique to each execution. diff --git a/src/Polly/IAsyncPolicy.TResult.cs b/src/Polly/IAsyncPolicy.TResult.cs index d7c10de9363..efc8ffab022 100644 --- a/src/Polly/IAsyncPolicy.TResult.cs +++ b/src/Polly/IAsyncPolicy.TResult.cs @@ -16,7 +16,7 @@ public interface IAsyncPolicy : IsPolicy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - IAsyncPolicy WithPolicyKey(String policyKey); + IAsyncPolicy WithPolicyKey(string policyKey); /// /// Executes the specified asynchronous action within the policy and returns the result. diff --git a/src/Polly/IAsyncPolicy.cs b/src/Polly/IAsyncPolicy.cs index f6ff5f9fff4..34da99e71cc 100644 --- a/src/Polly/IAsyncPolicy.cs +++ b/src/Polly/IAsyncPolicy.cs @@ -15,7 +15,7 @@ public interface IAsyncPolicy : IsPolicy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - IAsyncPolicy WithPolicyKey(String policyKey); + IAsyncPolicy WithPolicyKey(string policyKey); /// /// Executes the specified asynchronous action within the policy. diff --git a/src/Polly/ISyncPolicy.TResult.cs b/src/Polly/ISyncPolicy.TResult.cs index 13ee2a9224b..6a8cb85c110 100644 --- a/src/Polly/ISyncPolicy.TResult.cs +++ b/src/Polly/ISyncPolicy.TResult.cs @@ -16,7 +16,7 @@ public interface ISyncPolicy : IsPolicy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - ISyncPolicy WithPolicyKey(String policyKey); + ISyncPolicy WithPolicyKey(string policyKey); /// /// Executes the specified action within the policy and returns the Result. diff --git a/src/Polly/ISyncPolicy.cs b/src/Polly/ISyncPolicy.cs index 87d1c28fce2..bbb750fa29b 100644 --- a/src/Polly/ISyncPolicy.cs +++ b/src/Polly/ISyncPolicy.cs @@ -14,7 +14,7 @@ public interface ISyncPolicy : IsPolicy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - ISyncPolicy WithPolicyKey(String policyKey); + ISyncPolicy WithPolicyKey(string policyKey); /// /// Executes the specified action within the policy. diff --git a/src/Polly/IsPolicy.cs b/src/Polly/IsPolicy.cs index bdfddc61381..ed3211a8983 100644 --- a/src/Polly/IsPolicy.cs +++ b/src/Polly/IsPolicy.cs @@ -1,6 +1,4 @@ -using System; - -namespace Polly +namespace Polly { /// /// A marker interface identifying Polly policies of all types, and containing properties common to all policies @@ -10,6 +8,6 @@ public interface IsPolicy /// /// A key intended to be unique to each policy instance, which is passed with executions as the property. /// - String PolicyKey { get; } + string PolicyKey { get; } } } diff --git a/src/Polly/Policy.ContextAndKeys.cs b/src/Polly/Policy.ContextAndKeys.cs index f9a7c7c1b15..bb72a103316 100644 --- a/src/Polly/Policy.ContextAndKeys.cs +++ b/src/Polly/Policy.ContextAndKeys.cs @@ -1,6 +1,4 @@ -using System; - -namespace Polly +namespace Polly { public abstract partial class Policy { @@ -9,7 +7,7 @@ public abstract partial class Policy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - public Policy WithPolicyKey(String policyKey) + public Policy WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; @@ -22,7 +20,7 @@ public Policy WithPolicyKey(String policyKey) /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - ISyncPolicy ISyncPolicy.WithPolicyKey(String policyKey) + ISyncPolicy ISyncPolicy.WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; @@ -38,7 +36,7 @@ public abstract partial class Policy /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - public Policy WithPolicyKey(String policyKey) + public Policy WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; @@ -51,7 +49,7 @@ public Policy WithPolicyKey(String policyKey) /// Must be called before the policy is first used. Can only be set once. /// /// The unique, used-definable key to assign to this instance. - ISyncPolicy ISyncPolicy.WithPolicyKey(String policyKey) + ISyncPolicy ISyncPolicy.WithPolicyKey(string policyKey) { if (policyKeyInternal != null) throw PolicyKeyMustBeImmutableException; diff --git a/src/Polly/PolicyBase.ContextAndKeys.cs b/src/Polly/PolicyBase.ContextAndKeys.cs index 828e3e6b7fc..2f584230ad4 100644 --- a/src/Polly/PolicyBase.ContextAndKeys.cs +++ b/src/Polly/PolicyBase.ContextAndKeys.cs @@ -8,12 +8,12 @@ public abstract partial class PolicyBase /// /// A key intended to be unique to each instance. /// - protected String policyKeyInternal; + protected string policyKeyInternal; /// /// A key intended to be unique to each instance, which is passed with executions as the property. /// - public String PolicyKey => policyKeyInternal ?? (policyKeyInternal = GetType().Name + "-" + KeyHelper.GuidPart()); + public string PolicyKey => policyKeyInternal ?? (policyKeyInternal = GetType().Name + "-" + KeyHelper.GuidPart()); internal static ArgumentException PolicyKeyMustBeImmutableException => new ArgumentException("PolicyKey cannot be changed once set; or (when using the default value after the PolicyKey property has been accessed.", "policyKey"); diff --git a/src/Polly/PolicyBuilder.cs b/src/Polly/PolicyBuilder.cs index a29b8b4103b..a00802069d5 100644 --- a/src/Polly/PolicyBuilder.cs +++ b/src/Polly/PolicyBuilder.cs @@ -22,10 +22,10 @@ internal PolicyBuilder(ExceptionPredicate exceptionPredicate) #region Hide object members /// - /// Returns a that represents this instance. + /// Returns a that represents this instance. /// /// - /// A that represents this instance. + /// A that represents this instance. /// [EditorBrowsable(EditorBrowsableState.Never)] public override string ToString() @@ -34,11 +34,11 @@ public override string ToString() } /// - /// Determines whether the specified is equal to this instance. + /// Determines whether the specified is equal to this instance. /// - /// The to compare with this instance. + /// The to compare with this instance. /// - /// true if the specified is equal to this instance; otherwise, false. + /// true if the specified is equal to this instance; otherwise, false. /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object obj) @@ -107,20 +107,20 @@ internal PolicyBuilder(ExceptionPredicates exceptionPredicates) #region Hide object members /// - /// Returns a that represents this instance. + /// Returns a that represents this instance. /// /// - /// A that represents this instance. + /// A that represents this instance. /// [EditorBrowsable(EditorBrowsableState.Never)] public override string ToString() => base.ToString(); /// - /// Determines whether the specified is equal to this instance. + /// Determines whether the specified is equal to this instance. /// - /// The to compare with this instance. + /// The to compare with this instance. /// - /// true if the specified is equal to this instance; otherwise, false. + /// true if the specified is equal to this instance; otherwise, false. /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object obj) => base.Equals(obj); diff --git a/src/Polly/Polly.csproj b/src/Polly/Polly.csproj index c147c0ff416..4161a9e5d0f 100644 --- a/src/Polly/Polly.csproj +++ b/src/Polly/Polly.csproj @@ -2,11 +2,11 @@ netstandard1.1;netstandard2.0;net461;net472 - 7.2.2 + 7.3.0 7.0.0.0 - 7.2.2.0 - 7.2.2.0 - 7.2.2 + 7.3.0.0 + 7.3.0.0 + 7.3.0 App vNext Copyright (c) $([System.DateTime]::Now.ToString(yyyy)), App vNext Polly is a library that allows developers to express resilience and transient fault handling policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner. @@ -44,7 +44,7 @@ Polly BSD-3-Clause https://github.com/App-vNext/Polly - Exception Handling Resilience Transient Fault Policy Circuit Breaker CircuitBreaker Retry Wait Cache Cache-aside Bulkhead Fallback Timeout Throttle Parallelization + Exception Handling Resilience Transient Fault Policy Circuit Breaker CircuitBreaker Retry Wait Cache Cache-aside Bulkhead Rate-limit Fallback Timeout Throttle Parallelization See https://github.com/App-vNext/Polly/blob/master/CHANGELOG.md for details diff --git a/src/Polly/RateLimit/AsyncRateLimitEngine.cs b/src/Polly/RateLimit/AsyncRateLimitEngine.cs new file mode 100644 index 00000000000..16f40caec77 --- /dev/null +++ b/src/Polly/RateLimit/AsyncRateLimitEngine.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Polly.RateLimit +{ + internal static class AsyncRateLimitEngine + { + internal static async Task ImplementationAsync( + IRateLimiter rateLimiter, + Func retryAfterFactory, + Func> action, + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext + ) + { + (bool permit, TimeSpan retryAfter) = rateLimiter.PermitExecution(); + + if (permit) + { + return await action(context, cancellationToken).ConfigureAwait(continueOnCapturedContext); + } + + if (retryAfterFactory != null) + { + return retryAfterFactory(retryAfter, context); + } + + throw new RateLimitRejectedException(retryAfter); + } + } +} diff --git a/src/Polly/RateLimit/AsyncRateLimitPolicy.cs b/src/Polly/RateLimit/AsyncRateLimitPolicy.cs new file mode 100644 index 00000000000..261a9ea3dee --- /dev/null +++ b/src/Polly/RateLimit/AsyncRateLimitPolicy.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Polly.RateLimit +{ + /// + /// A rate-limit policy that can be applied to asynchronous delegates. + /// + public class AsyncRateLimitPolicy : AsyncPolicy, IRateLimitPolicy + { + private readonly IRateLimiter _rateLimiter; + + internal AsyncRateLimitPolicy(IRateLimiter rateLimiter) + { + _rateLimiter = rateLimiter ?? throw new ArgumentNullException(nameof(rateLimiter)); + } + + /// + [DebuggerStepThrough] + protected override Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, + bool continueOnCapturedContext) + => AsyncRateLimitEngine.ImplementationAsync(_rateLimiter, null, action, context, cancellationToken, continueOnCapturedContext); + } + + /// + /// A rate-limit policy that can be applied to asynchronous delegates returning a value of type . + /// + public class AsyncRateLimitPolicy : AsyncPolicy, IRateLimitPolicy + { + private readonly IRateLimiter _rateLimiter; + private readonly Func _retryAfterFactory; + + internal AsyncRateLimitPolicy( + IRateLimiter rateLimiter, + Func retryAfterFactory) + { + _rateLimiter = rateLimiter ?? throw new ArgumentNullException(nameof(rateLimiter)); + _retryAfterFactory = retryAfterFactory; + } + + /// + [DebuggerStepThrough] + protected override Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, + bool continueOnCapturedContext) + => AsyncRateLimitEngine.ImplementationAsync(_rateLimiter, _retryAfterFactory, action, context, cancellationToken, continueOnCapturedContext); + } +} \ No newline at end of file diff --git a/src/Polly/RateLimit/AsyncRateLimitSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitSyntax.cs new file mode 100644 index 00000000000..acca2974716 --- /dev/null +++ b/src/Polly/RateLimit/AsyncRateLimitSyntax.cs @@ -0,0 +1,43 @@ +using System; +using Polly.RateLimit; + +namespace Polly +{ + public partial class Policy + { + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan) + { + return RateLimitAsync(numberOfExecutions, perTimeSpan, 1); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while). + /// This equates to the bucket-capacity of a token-bucket implementation. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst) + { + if (numberOfExecutions < 1) throw new ArgumentOutOfRangeException(nameof(numberOfExecutions), numberOfExecutions, $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(maxBurst), maxBurst, $"{nameof(maxBurst)} must be an integer greater than or equal to 1."); + + IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions), maxBurst); + + return new AsyncRateLimitPolicy(rateLimiter); + } + } +} diff --git a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs new file mode 100644 index 00000000000..c91f9a7c088 --- /dev/null +++ b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs @@ -0,0 +1,83 @@ +using System; +using Polly.RateLimit; + +namespace Polly +{ + public partial class Policy + { + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan) + { + return RateLimitAsync(numberOfExecutions, perTimeSpan, null); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// An (optional) factory to express the recommended retry-after time back to the caller, when an operation is rate-limited. + /// If null, a with property will be thrown to indicate rate-limiting. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan, + Func retryAfterFactory) + { + return RateLimitAsync(numberOfExecutions, perTimeSpan, 1, retryAfterFactory); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while). + /// This equates to the bucket-capacity of a token-bucket implementation. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst) + { + return RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst, null); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given, + /// with a maximum burst size of + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while). + /// This equates to the bucket-capacity of a token-bucket implementation. + /// An (optional) factory to use to express retry-after back to the caller, when an operation is rate-limited. + /// If null, a with property will be thrown to indicate rate-limiting. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst, + Func retryAfterFactory) + { + if (numberOfExecutions < 1) throw new ArgumentOutOfRangeException(nameof(numberOfExecutions), numberOfExecutions, $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(maxBurst), maxBurst, $"{nameof(maxBurst)} must be an integer greater than or equal to 1."); + + IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions), maxBurst); + + return new AsyncRateLimitPolicy(rateLimiter, retryAfterFactory); + } + } +} diff --git a/src/Polly/RateLimit/IRateLimitPolicy.cs b/src/Polly/RateLimit/IRateLimitPolicy.cs new file mode 100644 index 00000000000..88c967e0c3c --- /dev/null +++ b/src/Polly/RateLimit/IRateLimitPolicy.cs @@ -0,0 +1,16 @@ +namespace Polly.RateLimit +{ + /// + /// Defines properties and methods common to all RateLimit policies. + /// + public interface IRateLimitPolicy : IsPolicy + { + } + + /// + /// Defines properties and methods common to all RateLimit policies generic-typed for executions returning results of type . + /// + public interface IRateLimitPolicy : IRateLimitPolicy + { + } +} diff --git a/src/Polly/RateLimit/IRateLimiter.cs b/src/Polly/RateLimit/IRateLimiter.cs new file mode 100644 index 00000000000..79e724f3acb --- /dev/null +++ b/src/Polly/RateLimit/IRateLimiter.cs @@ -0,0 +1,16 @@ +using System; + +namespace Polly.RateLimit +{ + /// + /// Defines methods to be provided by a rate-limiter used in a Polly + /// + internal interface IRateLimiter + { + /// + /// Returns whether the execution is permitted; if not, returns what should be waited before retrying. + /// Calling this method consumes an execution permit if one is available: a caller receiving a return value true should make an execution. + /// + (bool permitExecution, TimeSpan retryAfter) PermitExecution(); + } +} diff --git a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs new file mode 100644 index 00000000000..b4b3dd54841 --- /dev/null +++ b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs @@ -0,0 +1,116 @@ +using System; +using System.Threading; +using Polly.Utilities; + +namespace Polly.RateLimit +{ + /// + /// A lock-free token-bucket rate-limiter for a Polly . + /// + internal sealed class LockFreeTokenBucketRateLimiter : IRateLimiter + { + private readonly long addTokenTickInterval; + private readonly long bucketCapacity; + + private long currentTokens; + + private long addNextTokenAtTicks; + +#if !NETSTANDARD2_0 + private SpinWait spinner = new(); +#endif + + /// + /// Creates an instance of + /// + /// How often one execution is permitted. + /// The capacity of the token bucket. + /// This equates to the maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while). + /// + public LockFreeTokenBucketRateLimiter(TimeSpan onePer, long bucketCapacity) + { + if (onePer <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(onePer), onePer, $"The {nameof(LockFreeTokenBucketRateLimiter)} must specify a positive TimeSpan for how often an execution is permitted."); + if (bucketCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(bucketCapacity), bucketCapacity, $"{nameof(bucketCapacity)} must be greater than or equal to 1."); + + addTokenTickInterval = onePer.Ticks; + this.bucketCapacity = bucketCapacity; + + currentTokens = bucketCapacity; + addNextTokenAtTicks = SystemClock.DateTimeOffsetUtcNow().Ticks + addTokenTickInterval; + } + + /// + /// Returns whether the execution is permitted; if not, returns what should be waited before retrying. + /// + public (bool permitExecution, TimeSpan retryAfter) PermitExecution() + { + while (true) + { + // Try to get a token. + long tokensAfterGrabOne = Interlocked.Decrement(ref currentTokens); + + if (tokensAfterGrabOne >= 0) + { + // We got a token: permit execution! + return (true, TimeSpan.Zero); + } + + // No tokens! We're rate-limited - unless we can refill the bucket. + long now = SystemClock.DateTimeOffsetUtcNow().Ticks; + long currentAddNextTokenAtTicks = Interlocked.Read(ref addNextTokenAtTicks); + long ticksTillAddNextToken = currentAddNextTokenAtTicks - now; + + if (ticksTillAddNextToken > 0) + { + // Not time to add tokens yet: we're rate-limited! + return (false, TimeSpan.FromTicks(ticksTillAddNextToken)); + } + + // Time to add tokens to the bucket! + + // We definitely need to add one token. In fact, if we haven't hit this bit of code for a while, we might be due to add a bunch of tokens. + long tokensMissedAdding = + // Passing addNextTokenAtTicks merits one token + 1 + + // And any whole token tick intervals further each merit another. + (-ticksTillAddNextToken / addTokenTickInterval); + + // We mustn't exceed bucket capacity though. + long tokensToAdd = Math.Min(bucketCapacity, tokensMissedAdding); + + // Work out when tokens would next be due to be added, if we add these tokens. + long newAddNextTokenAtTicks = currentAddNextTokenAtTicks + (tokensToAdd * addTokenTickInterval); + // But if we were way overdue refilling the bucket (there was inactivity for a while), that value would be out-of-date: the next time we add tokens must be at least addTokenTickInterval from now. + newAddNextTokenAtTicks = Math.Max(newAddNextTokenAtTicks, now + addTokenTickInterval); + + // Now see if we win the race to add these tokens. Other threads might be racing through this code at the same time: only one thread must add the tokens! + if (Interlocked.CompareExchange(ref addNextTokenAtTicks, newAddNextTokenAtTicks, currentAddNextTokenAtTicks) == currentAddNextTokenAtTicks) + { + // We won the race to add the tokens! + + // Theoretically we want to add tokensToAdd tokens. But in fact we don't do that. + // We want to claim one of those tokens for ourselves - there's no way we're going to add it but let another thread snatch it from under our nose. + // (Doing that could leave this thread looping round adding tokens for ever which other threads just snatch - would lead to odd observed behaviour.) + + // So in fact we add (tokensToAdd - 1) tokens (ie we consume one), and return, permitting this execution. + + // The advantage of only adding tokens when the bucket is empty is that we can now hard set the new amount of tokens (Interlocked.Exchange) without caring if other threads have simultaneously been taking or adding tokens. + // (If we added a token per addTokenTickInterval to a non-empty bucket, the reasoning about not overflowing the bucket seems harder.) + Interlocked.Exchange(ref currentTokens, tokensToAdd - 1); + return (true, TimeSpan.Zero); + } + else + { + // We didn't win the race to add the tokens. BUT because it _was_ time to add tokens, another thread must have won that race and have added/be adding tokens, so there _may_ be more tokens, so loop and try again. + + // We want any thread refilling the bucket to have a chance to do so before we try to grab the next token. +#if NETSTANDARD2_0 + Thread.Sleep(0); +#else + spinner.SpinOnce(); +#endif + } + } + } + } +} diff --git a/src/Polly/RateLimit/RateLimitEngine.cs b/src/Polly/RateLimit/RateLimitEngine.cs new file mode 100644 index 00000000000..a96cbb2835d --- /dev/null +++ b/src/Polly/RateLimit/RateLimitEngine.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; + +namespace Polly.RateLimit +{ + internal static class RateLimitEngine + { + internal static TResult Implementation( + IRateLimiter rateLimiter, + Func retryAfterFactory, + Func action, + Context context, + CancellationToken cancellationToken + ) + { + (bool permit, TimeSpan retryAfter) = rateLimiter.PermitExecution(); + + if (permit) + { + return action(context, cancellationToken); + } + + if (retryAfterFactory != null) + { + return retryAfterFactory(retryAfter, context); + } + + throw new RateLimitRejectedException(retryAfter); + } + } +} \ No newline at end of file diff --git a/src/Polly/RateLimit/RateLimitPolicy.cs b/src/Polly/RateLimit/RateLimitPolicy.cs new file mode 100644 index 00000000000..aa0106f94f6 --- /dev/null +++ b/src/Polly/RateLimit/RateLimitPolicy.cs @@ -0,0 +1,46 @@ +using System; +using System.Diagnostics; +using System.Threading; + +namespace Polly.RateLimit +{ + /// + /// A rate-limit policy that can be applied to synchronous delegates. + /// + public class RateLimitPolicy : Policy, IRateLimitPolicy + { + private readonly IRateLimiter _rateLimiter; + + internal RateLimitPolicy(IRateLimiter rateLimiter) + { + _rateLimiter = rateLimiter ?? throw new ArgumentNullException(nameof(rateLimiter)); + } + + /// + [DebuggerStepThrough] + protected override TResult Implementation(Func action, Context context, CancellationToken cancellationToken) + => RateLimitEngine.Implementation(_rateLimiter, null, action, context, cancellationToken); + } + + /// + /// A rate-limit policy that can be applied to synchronous delegates returning a value of type . + /// + public class RateLimitPolicy : Policy, IRateLimitPolicy + { + private readonly IRateLimiter _rateLimiter; + private readonly Func _retryAfterFactory; + + internal RateLimitPolicy( + IRateLimiter rateLimiter, + Func retryAfterFactory) + { + _rateLimiter = rateLimiter ?? throw new ArgumentNullException(nameof(rateLimiter)); + _retryAfterFactory = retryAfterFactory; + } + + /// + [DebuggerStepThrough] + protected override TResult Implementation(Func action, Context context, CancellationToken cancellationToken) + => RateLimitEngine.Implementation(_rateLimiter, _retryAfterFactory, action, context, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Polly/RateLimit/RateLimitRejectedException.cs b/src/Polly/RateLimit/RateLimitRejectedException.cs new file mode 100644 index 00000000000..d30b1557ab9 --- /dev/null +++ b/src/Polly/RateLimit/RateLimitRejectedException.cs @@ -0,0 +1,79 @@ +using System; +#if NETSTANDARD2_0 +using System.Runtime.Serialization; +#endif + +namespace Polly.RateLimit +{ + /// + /// Exception thrown when a delegate executed through a is rate-limited. + /// +#if NETSTANDARD2_0 + [Serializable] +#endif + public class RateLimitRejectedException : ExecutionRejectedException + { + /// + /// The timespan after which the operation may be retried. + /// + public TimeSpan RetryAfter { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The timespan after which the operation may be retried. + public RateLimitRejectedException(TimeSpan retryAfter) : this(retryAfter, DefaultMessage(retryAfter)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The timespan after which the operation may be retried. + /// The inner exception. + public RateLimitRejectedException(TimeSpan retryAfter, Exception innerException) : base(DefaultMessage(retryAfter), innerException) + { + SetRetryAfter(retryAfter); + } + + /// + /// Initializes a new instance of the class. + /// + /// The timespan after which the operation may be retried. + /// The message. + public RateLimitRejectedException(TimeSpan retryAfter, string message) : base(message) + { + SetRetryAfter(retryAfter); + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The timespan after which the operation may be retried. + /// The inner exception. + public RateLimitRejectedException(TimeSpan retryAfter, string message, Exception innerException) : base(message, innerException) + { + SetRetryAfter(retryAfter); + } + + private void SetRetryAfter(TimeSpan retryAfter) + { + if (retryAfter < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(retryAfter), retryAfter, $"The {nameof(retryAfter)} parameter must be a TimeSpan greater than or equal to TimeSpan.Zero."); + RetryAfter = retryAfter; + } + + private static string DefaultMessage(TimeSpan retryAfter) => $"The operation has been rate-limited and should be retried after {retryAfter}"; + +#if NETSTANDARD2_0 + /// + /// Initializes a new instance of the class. + /// + /// The information. + /// The context. + protected RateLimitRejectedException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } +#endif + } +} diff --git a/src/Polly/RateLimit/RateLimitSyntax.cs b/src/Polly/RateLimit/RateLimitSyntax.cs new file mode 100644 index 00000000000..d172ef7c73b --- /dev/null +++ b/src/Polly/RateLimit/RateLimitSyntax.cs @@ -0,0 +1,43 @@ +using System; +using Polly.RateLimit; + +namespace Polly +{ + public partial class Policy + { + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The policy instance. + public static RateLimitPolicy RateLimit( + int numberOfExecutions, + TimeSpan perTimeSpan) + { + return RateLimit(numberOfExecutions, perTimeSpan, 1); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while). + /// This equates to the bucket-capacity of a token-bucket implementation. + /// The policy instance. + public static RateLimitPolicy RateLimit( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst) + { + if (numberOfExecutions < 1) throw new ArgumentOutOfRangeException(nameof(numberOfExecutions), numberOfExecutions, $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(maxBurst), maxBurst, $"{nameof(maxBurst)} must be an integer greater than or equal to 1."); + + IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions), maxBurst); + + return new RateLimitPolicy(rateLimiter); + } + } +} diff --git a/src/Polly/RateLimit/RateLimitTResultSyntax.cs b/src/Polly/RateLimit/RateLimitTResultSyntax.cs new file mode 100644 index 00000000000..eb471d68451 --- /dev/null +++ b/src/Polly/RateLimit/RateLimitTResultSyntax.cs @@ -0,0 +1,83 @@ +using System; +using Polly.RateLimit; + +namespace Polly +{ + public partial class Policy + { + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The policy instance. + public static RateLimitPolicy RateLimit( + int numberOfExecutions, + TimeSpan perTimeSpan) + { + return RateLimit(numberOfExecutions, perTimeSpan, null); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// An (optional) factory to express the recommended retry-after time back to the caller, when an operation is rate-limited. + /// If null, a with property will be thrown to indicate rate-limiting. + /// The policy instance. + public static RateLimitPolicy RateLimit( + int numberOfExecutions, + TimeSpan perTimeSpan, + Func retryAfterFactory) + { + return RateLimit(numberOfExecutions, perTimeSpan, 1, retryAfterFactory); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given. + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while). + /// This equates to the bucket-capacity of a token-bucket implementation. + /// The policy instance. + public static RateLimitPolicy RateLimit( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst) + { + return RateLimit(numberOfExecutions, perTimeSpan, maxBurst, null); + } + + /// + /// Builds a RateLimit that will rate-limit executions to per the timespan given, + /// with a maximum burst size of + /// + /// The type of return values this policy will handle. + /// The number of executions (call it N) permitted per timespan. + /// How often N executions are permitted. + /// The maximum number of executions that will be permitted in a single burst (for example if none have been executed for a while). + /// This equates to the bucket-capacity of a token-bucket implementation. + /// An (optional) factory to use to express retry-after back to the caller, when an operation is rate-limited. + /// If null, a with property will be thrown to indicate rate-limiting. + /// The policy instance. + public static RateLimitPolicy RateLimit( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst, + Func retryAfterFactory) + { + if (numberOfExecutions < 1) throw new ArgumentOutOfRangeException(nameof(numberOfExecutions), numberOfExecutions, $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), perTimeSpan, $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(maxBurst), maxBurst, $"{nameof(maxBurst)} must be an integer greater than or equal to 1."); + + IRateLimiter rateLimiter = RateLimiterFactory.Create(TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions), maxBurst); + + return new RateLimitPolicy(rateLimiter, retryAfterFactory); + } + } +} diff --git a/src/Polly/RateLimit/RateLimiterFactory.cs b/src/Polly/RateLimit/RateLimiterFactory.cs new file mode 100644 index 00000000000..e4f01ee29bc --- /dev/null +++ b/src/Polly/RateLimit/RateLimiterFactory.cs @@ -0,0 +1,10 @@ +using System; + +namespace Polly.RateLimit +{ + internal static class RateLimiterFactory + { + public static IRateLimiter Create(TimeSpan onePer, int bucketCapacity) + => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); + } +} diff --git a/src/Polly/Registry/PolicyRegistry.cs b/src/Polly/Registry/PolicyRegistry.cs index 1059f97db4b..a724d4cb835 100644 --- a/src/Polly/Registry/PolicyRegistry.cs +++ b/src/Polly/Registry/PolicyRegistry.cs @@ -7,7 +7,7 @@ namespace Polly.Registry { /// /// - /// Stores a registry of and policy pairs. + /// Stores a registry of and policy pairs. /// /// Uses ConcurrentDictionary to store the collection. public class PolicyRegistry : IConcurrentPolicyRegistry @@ -15,7 +15,7 @@ public class PolicyRegistry : IConcurrentPolicyRegistry private readonly IDictionary _registry = new ConcurrentDictionary(); /// - /// Creates a registry of policies with keys. + /// Creates a registry of policies with keys. /// public PolicyRegistry() { @@ -26,7 +26,7 @@ public PolicyRegistry() } /// - /// Creates a registry of policies with keys. + /// Creates a registry of policies with keys. /// This internal constructor exists solely to facilitate testing of the GetEnumerator() methods, which allow us to support collection initialisation syntax. /// /// a dictionary containing keys and policies used for testing. diff --git a/src/Polly/Timeout/TimeoutRejectedException.cs b/src/Polly/Timeout/TimeoutRejectedException.cs index c2f6ec95c3a..81149e21340 100644 --- a/src/Polly/Timeout/TimeoutRejectedException.cs +++ b/src/Polly/Timeout/TimeoutRejectedException.cs @@ -24,7 +24,7 @@ public TimeoutRejectedException() /// Initializes a new instance of the class. /// /// The message. - public TimeoutRejectedException(String message) : base(message) + public TimeoutRejectedException(string message) : base(message) { } @@ -33,7 +33,7 @@ public TimeoutRejectedException(String message) : base(message) /// /// The message. /// The inner exception. - public TimeoutRejectedException(String message, Exception innerException) : base(message, innerException) + public TimeoutRejectedException(string message, Exception innerException) : base(message, innerException) { } diff --git a/src/Polly/Utilities/KeyHelper.cs b/src/Polly/Utilities/KeyHelper.cs index cbb9177c469..1aff004fb76 100644 --- a/src/Polly/Utilities/KeyHelper.cs +++ b/src/Polly/Utilities/KeyHelper.cs @@ -4,6 +4,6 @@ namespace Polly.Utilities { internal static class KeyHelper { - public static String GuidPart() => Guid.NewGuid().ToString().Substring(0, 8); + public static string GuidPart() => Guid.NewGuid().ToString().Substring(0, 8); } }