From a506f0232b1fcb5032b7ed7ddcfde3220856a098 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Sat, 23 Mar 2019 20:18:57 +0000 Subject: [PATCH 01/27] Add core RateLimiter implementations --- src/Polly/RateLimit/IRateLimiter.cs | 16 +++ .../LockBasedTokenBucketRateLimiter.cs | 91 ++++++++++++++ .../LockFreeTokenBucketRateLimiter.cs | 113 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/Polly/RateLimit/IRateLimiter.cs create mode 100644 src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs create mode 100644 src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs diff --git a/src/Polly/RateLimit/IRateLimiter.cs b/src/Polly/RateLimit/IRateLimiter.cs new file mode 100644 index 0000000000..9136bbcc20 --- /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 + /// + public 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/LockBasedTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs new file mode 100644 index 0000000000..8c299a8ed5 --- /dev/null +++ b/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs @@ -0,0 +1,91 @@ +using System; +using Polly.Utilities; + +namespace Polly.RateLimit +{ + /// + /// A lock-based token-bucket rate-limiter for a Polly . + /// + public class LockBasedTokenBucketRateLimiter : IRateLimiter + { + private readonly long addTokenTickInterval; + private readonly long bucketCapacity; + + private long currentTokens; + + private long addNextTokenAtTicks; + + private readonly object _lock = new object(); + + /// + /// 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 LockBasedTokenBucketRateLimiter(TimeSpan onePer, long bucketCapacity) + { + if (onePer <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(onePer), $"The ${nameof(LockFreeTokenBucketRateLimiter)} must specify a positive TimeSpan for how often an execution is permitted."); + if (bucketCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(bucketCapacity), $"The ${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() + { + using (TimedLock.Lock(_lock)) + { + // Try to get a token. + if (--currentTokens >= 0) + { + // We got a token: permit execution! + return (true, TimeSpan.Zero); + } + else + { + // No tokens! We're rate-limited - unless we can refill the bucket. + long now = SystemClock.DateTimeOffsetUtcNow().Ticks; + + long ticksTillAddNextToken = addNextTokenAtTicks - now; + if (ticksTillAddNextToken > 0) + { + // Not time to add tokens yet: we're rate-limited! + return (false, TimeSpan.FromTicks(ticksTillAddNextToken)); + } + else + { + // 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 = -ticksTillAddNextToken / addTokenTickInterval; + // For clarity: it's time to add 1 + tokensMissedAdding tokens to the bucket. + + // We mustn't exceed bucket capacity though. + long tokensToAdd = Math.Min(bucketCapacity, 1 + tokensMissedAdding); + + // Work out when tokens would next be due to be added, if we add these tokens. + long newAddNextTokenAtTicks = addNextTokenAtTicks + 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); + + addNextTokenAtTicks = newAddNextTokenAtTicks; + + // 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. + // So in fact we add (tokensToAdd - 1) tokens (ie we consume one), and return, permitting this execution. + currentTokens = tokensToAdd - 1; + return (true, TimeSpan.Zero); + } + } + } + + } + } +} diff --git a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs new file mode 100644 index 0000000000..9997725789 --- /dev/null +++ b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading; +using Polly.Utilities; + +namespace Polly.RateLimit +{ + /// + /// A lock-free token-bucket rate-limiter for a Polly . + /// + public class LockFreeTokenBucketRateLimiter : IRateLimiter + { + private readonly long addTokenTickInterval; + private readonly long bucketCapacity; + + private long currentTokens; + + private long addNextTokenAtTicks; + +#if !NETSTANDARD2_0 + SpinWait spinner = new SpinWait(); +#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), $"The ${nameof(LockFreeTokenBucketRateLimiter)} must specify a positive TimeSpan for how often an execution is permitted."); + if (bucketCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(bucketCapacity), $"The ${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 = -ticksTillAddNextToken / addTokenTickInterval; + // For clarity: it's time to add 1 + tokensMissedAdding tokens to the bucket. + + // We mustn't exceed bucket capacity though. + long tokensToAdd = Math.Min(bucketCapacity, 1 + 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 + } + } + } + } +} From e31aa32bc413557f81ed70fd8095f0498581d4c7 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Sat, 23 Mar 2019 20:19:28 +0000 Subject: [PATCH 02/27] Add example async TResult rate-limiter policy implementation --- src/Polly/RateLimit/AsyncRateLimitEngine.cs | 31 ++++++++ src/Polly/RateLimit/AsyncRateLimitPolicy.cs | 49 ++++++++++++ src/Polly/RateLimit/IRateLimitPolicy.cs | 17 ++++ .../RateLimit/RateLimitRejectedException.cs | 78 +++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 src/Polly/RateLimit/AsyncRateLimitEngine.cs create mode 100644 src/Polly/RateLimit/AsyncRateLimitPolicy.cs create mode 100644 src/Polly/RateLimit/IRateLimitPolicy.cs create mode 100644 src/Polly/RateLimit/RateLimitRejectedException.cs diff --git a/src/Polly/RateLimit/AsyncRateLimitEngine.cs b/src/Polly/RateLimit/AsyncRateLimitEngine.cs new file mode 100644 index 0000000000..32973da717 --- /dev/null +++ b/src/Polly/RateLimit/AsyncRateLimitEngine.cs @@ -0,0 +1,31 @@ +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(context); + } + + throw new RateLimitRejectedException(retryAfter); + } + } +} diff --git a/src/Polly/RateLimit/AsyncRateLimitPolicy.cs b/src/Polly/RateLimit/AsyncRateLimitPolicy.cs new file mode 100644 index 0000000000..8410feaae3 --- /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 NullReferenceException(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 NullReferenceException(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/IRateLimitPolicy.cs b/src/Polly/RateLimit/IRateLimitPolicy.cs new file mode 100644 index 0000000000..08127fde2f --- /dev/null +++ b/src/Polly/RateLimit/IRateLimitPolicy.cs @@ -0,0 +1,17 @@ +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/RateLimitRejectedException.cs b/src/Polly/RateLimit/RateLimitRejectedException.cs new file mode 100644 index 0000000000..3b7fbda1d8 --- /dev/null +++ b/src/Polly/RateLimit/RateLimitRejectedException.cs @@ -0,0 +1,78 @@ +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), $"The {nameof(retryAfter)} parameter must be a TimeSpan greater than or equal to TimeSpan.Zero."); + } + + 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 + } +} From 12a276a783c2ead02dd5f44b495a322f5dd08d6b Mon Sep 17 00:00:00 2001 From: reisenberger Date: Sat, 23 Mar 2019 20:19:56 +0000 Subject: [PATCH 03/27] Add example syntax --- .../RateLimit/AsyncRateLimitTResultSyntax.cs | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs diff --git a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs new file mode 100644 index 0000000000..e20219d39f --- /dev/null +++ b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs @@ -0,0 +1,145 @@ +using System; +using Polly.RateLimit; + +namespace Polly +{ + public partial class Policy + { + + /* Maybe these commented out overloads with TimeSpan permitOneExecutionPer are not as intuitive as the overloads left uncommented. + + /// + /// Builds a RateLimit that will rate-limit executions to one per the timespan given. + /// A will be thrown to indicate rate-limiting. + /// + /// How often one execution is permitted. + /// The type of return values this policy will handle. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync(TimeSpan permitOneExecutionPer) + { + return RateLimitAsync(permitOneExecutionPer, null); + } + + /// + /// Builds a RateLimit that will rate-limit executions to one per the timespan given, + /// with a maximum burst size of . + /// A will be thrown to indicate rate-limiting. + /// + /// How often one execution is 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 type of return values this policy will handle. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync(TimeSpan permitOneExecutionPer, int maxBurst) + { + return RateLimitAsync(permitOneExecutionPer, maxBurst, null); + } + + /// + /// Builds a RateLimit that will rate-limit executions to one per the timespan given. + /// + /// The type of return values this policy will handle. + /// How often one execution is permitted. + /// An (optional) factory to use to express retry-after back to the caller, when an operation is rate-limited. + /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + TimeSpan permitOneExecutionPer, + Func retryAfterFactory) + { + return RateLimitAsync(permitOneExecutionPer, 1, retryAfterFactory); + } + + /// + /// Builds a RateLimit that will rate-limit executions to one per the timespan given, + /// with a maximum burst size of + /// + /// The type of return values this policy will handle. + /// How often one execution is 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. + /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + TimeSpan permitOneExecutionPer, + int maxBurst, + Func retryAfterFactory) + { + return RateLimitAsync(new TokenBucketRateLimiter(permitOneExecutionPer, maxBurst), retryAfterFactory); + } + */ + + private static readonly Func DefaultRateLimiterFactory = (onePer, bucketCapacity) => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); + // private readonly Func DefaultRateLimiterFactory = (onePer, bucketCapacity) => new LockBasedTokenBucketRateLimiter(onePer, bucketCapacity); + + /// + /// 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 use to express retry-after back to the caller, when an operation is rate-limited. + /// This parameter can be null. If null, a 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, + /// 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. + /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst, + Func retryAfterFactory) + { + return RateLimitAsync(DefaultRateLimiterFactory(TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions), maxBurst), retryAfterFactory); + } + + /// + /// Builds a RateLimit that will rate-limit executions with the provided . + /// + /// The type of return values this policy will handle. + /// The rate-limiter to use to determine whether the execution is permitted. + /// An (optional) factory to use to express retry-after back to the caller, when an operation is rate-limited. + /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. + /// The policy instance. + public static AsyncRateLimitPolicy RateLimitAsync( + IRateLimiter rateLimiter, + Func retryAfterFactory) + { + if (rateLimiter == null) throw new NullReferenceException(nameof(rateLimiter)); + + return new AsyncRateLimitPolicy(rateLimiter, retryAfterFactory); + } + } +} From 6e2da28d0eda01bdb2552682dc88470ca78ac033 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Sun, 24 Mar 2019 07:49:31 +0000 Subject: [PATCH 04/27] Make the retryAfterFactory take Timespan as an input parameter! --- src/Polly/RateLimit/AsyncRateLimitEngine.cs | 4 ++-- src/Polly/RateLimit/AsyncRateLimitPolicy.cs | 4 ++-- src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs | 13 ++++++++----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Polly/RateLimit/AsyncRateLimitEngine.cs b/src/Polly/RateLimit/AsyncRateLimitEngine.cs index 32973da717..703eb56837 100644 --- a/src/Polly/RateLimit/AsyncRateLimitEngine.cs +++ b/src/Polly/RateLimit/AsyncRateLimitEngine.cs @@ -8,7 +8,7 @@ internal static class AsyncRateLimitEngine { internal static async Task ImplementationAsync( IRateLimiter rateLimiter, - Func retryAfterFactory, + Func retryAfterFactory, Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext ) @@ -22,7 +22,7 @@ internal static async Task ImplementationAsync( if (retryAfterFactory != null) { - return retryAfterFactory(context); + return retryAfterFactory(retryAfter, context); } throw new RateLimitRejectedException(retryAfter); diff --git a/src/Polly/RateLimit/AsyncRateLimitPolicy.cs b/src/Polly/RateLimit/AsyncRateLimitPolicy.cs index 8410feaae3..deb294f85a 100644 --- a/src/Polly/RateLimit/AsyncRateLimitPolicy.cs +++ b/src/Polly/RateLimit/AsyncRateLimitPolicy.cs @@ -30,11 +30,11 @@ protected override Task ImplementationAsync(Func : AsyncPolicy, IRateLimitPolicy { private readonly IRateLimiter _rateLimiter; - private readonly Func _retryAfterFactory; + private readonly Func _retryAfterFactory; internal AsyncRateLimitPolicy( IRateLimiter rateLimiter, - Func retryAfterFactory) + Func retryAfterFactory) { _rateLimiter = rateLimiter ?? throw new NullReferenceException(nameof(rateLimiter)); _retryAfterFactory = retryAfterFactory; diff --git a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs index e20219d39f..5ffdbeea04 100644 --- a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs +++ b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs @@ -1,4 +1,7 @@ using System; +using System.Globalization; +using System.Net; +using System.Net.Http; using Polly.RateLimit; namespace Polly @@ -45,7 +48,7 @@ public static AsyncRateLimitPolicy RateLimitAsync(TimeSpan per /// The policy instance. public static AsyncRateLimitPolicy RateLimitAsync( TimeSpan permitOneExecutionPer, - Func retryAfterFactory) + Func retryAfterFactory) { return RateLimitAsync(permitOneExecutionPer, 1, retryAfterFactory); } @@ -64,7 +67,7 @@ public static AsyncRateLimitPolicy RateLimitAsync( public static AsyncRateLimitPolicy RateLimitAsync( TimeSpan permitOneExecutionPer, int maxBurst, - Func retryAfterFactory) + Func retryAfterFactory) { return RateLimitAsync(new TokenBucketRateLimiter(permitOneExecutionPer, maxBurst), retryAfterFactory); } @@ -99,7 +102,7 @@ public static AsyncRateLimitPolicy RateLimitAsync( public static AsyncRateLimitPolicy RateLimitAsync( int numberOfExecutions, TimeSpan perTimeSpan, - Func retryAfterFactory) + Func retryAfterFactory) { return RateLimitAsync(numberOfExecutions, perTimeSpan, 1, retryAfterFactory); } @@ -120,7 +123,7 @@ public static AsyncRateLimitPolicy RateLimitAsync( int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst, - Func retryAfterFactory) + Func retryAfterFactory) { return RateLimitAsync(DefaultRateLimiterFactory(TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions), maxBurst), retryAfterFactory); } @@ -135,7 +138,7 @@ public static AsyncRateLimitPolicy RateLimitAsync( /// The policy instance. public static AsyncRateLimitPolicy RateLimitAsync( IRateLimiter rateLimiter, - Func retryAfterFactory) + Func retryAfterFactory) { if (rateLimiter == null) throw new NullReferenceException(nameof(rateLimiter)); From f5435e454fa3ca5b7677372be4b9f05d360da0e0 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 06:11:05 +0100 Subject: [PATCH 05/27] Initial LockFreeTokenBucketRateLimiterTests --- .../LockFreeTokenBucketRateLimiterTests.cs | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs diff --git a/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs new file mode 100644 index 0000000000..bc299e7c47 --- /dev/null +++ b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs @@ -0,0 +1,220 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Polly.RateLimit; +using Polly.Utilities; +using FluentAssertions.Execution; +using Xunit; +using Xunit.Sdk; + +namespace Polly.Specs.RateLimit +{ + + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + + public class LockFreeTokenBucketRateLimiterTests : IDisposable + { + public IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) + => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); + + [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); + + // Act - first execution after initialising should always be permitted. + (bool permitExecution, TimeSpan retryAfter) canExecute = rateLimiter.PermitExecution(); + + // Assert. + canExecute.permitExecution.Should().BeTrue(); + canExecute.retryAfter.Should().Be(TimeSpan.Zero); + + // Arrange + // (do nothing - time not advanced) + + // Act - try another execution + canExecute = rateLimiter.PermitExecution(); + + // Assert - should be blocked - time not advanced. + canExecute.permitExecution.Should().BeFalse(); + canExecute.retryAfter.Should().Be(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(long bucketCapacity) + { + FixClock(); + + // Arrange. + TimeSpan onePer = TimeSpan.FromSeconds(1); + var rateLimiter = GetRateLimiter(onePer, bucketCapacity); + + // Act - should be able to successfully take bucketCapacity items. + AssertCanTakeNItems(rateLimiter, bucketCapacity); + + // After that, should not be able to take any items (given time not advanced). + + // Act + (bool permitExecution, TimeSpan retryAfter) canExecute = rateLimiter.PermitExecution(); + + // Assert - should be blocked - time not advanced. + canExecute.permitExecution.Should().BeFalse(); + canExecute.retryAfter.Should().BeGreaterThan(TimeSpan.Zero); + } + + + [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) + { + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + var rateLimiter = GetRateLimiter(onePer, bucketCapacity); + + FixClock(); + + // Arrange - spend the initial bucket capacity. + AssertCanTakeNItems(rateLimiter, bucketCapacity); + + // Act-Assert - repeatedly advance the clock towards the interval but not quite - then to the interval + int experimentRepeats = bucketCapacity * 3; + const int shortfallFromInterval = 1; + long notQuiteInterval = onePer.Ticks - shortfallFromInterval; + (bool permitExecution, TimeSpan retryAfter) canExecute; + for (int i = 0; i < experimentRepeats; i++) + { + // Arrange - Advance clock not quite to the interval + AdvanceClock(notQuiteInterval); + // Act + canExecute = rateLimiter.PermitExecution(); + // Assert - should not quite be able to issue another token + canExecute.permitExecution.Should().BeFalse(); + canExecute.retryAfter.Should().Be(TimeSpan.FromTicks(shortfallFromInterval)); + + // Arrange - Advance clock to the interval + AdvanceClock(shortfallFromInterval); + // Act + canExecute = rateLimiter.PermitExecution(); + // Assert - should be able to issue another token + canExecute.permitExecution.Should().BeTrue(); + canExecute.retryAfter.Should().Be(TimeSpan.Zero); + + // Act + canExecute = rateLimiter.PermitExecution(); + // Assert - cannot get another token straight away + canExecute.permitExecution.Should().BeFalse(); + canExecute.retryAfter.Should().BeGreaterThan(TimeSpan.Zero); + } + } + + [Theory] + [InlineData(2)] + [InlineData(5)] + [InlineData(100)] + public void Given_immediate_parallel_contention_ratelimiter_still_only_permits_one(int parallelContention) + { + // 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(5), () => 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); + } + + /// + /// 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. + private void Within(TimeSpan timeSpan, Action actionContainingAssertions) + { + TimeSpan retryInterval = TimeSpan.FromTicks(Math.Min(TimeSpan.FromSeconds(0.2).Ticks, timeSpan.Ticks/10)); + + 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); + } + } + } + + private static void FixClock() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + SystemClock.DateTimeOffsetUtcNow = () => now; + } + + private static void AdvanceClock(long advanceTicks) + { + DateTimeOffset now = SystemClock.DateTimeOffsetUtcNow(); + SystemClock.DateTimeOffsetUtcNow = () => now + TimeSpan.FromTicks(advanceTicks); + } + + private static void AssertCanTakeNItems(IRateLimiter rateLimiter, long expectedTake) + { + (bool permitExecution, TimeSpan retryAfter) canExecute; + for (int take = 0; take < expectedTake; take++) + { + // Act + canExecute = rateLimiter.PermitExecution(); + + // Assert. + canExecute.permitExecution.Should().BeTrue(); + canExecute.retryAfter.Should().Be(TimeSpan.Zero); + } + } + + public void Dispose() + { + SystemClock.Reset(); + } + + } +} From adbefab8d39813d5ade676e690a7d8e9fb390f7e Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 06:29:41 +0100 Subject: [PATCH 06/27] Factor out test helpers --- .../RateLimit/IRateLimiterExtensions.cs | 40 ++++++++++ .../LockFreeTokenBucketRateLimiterTests.cs | 77 ++++++------------- 2 files changed, 62 insertions(+), 55 deletions(-) create mode 100644 src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs diff --git a/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs b/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs new file mode 100644 index 0000000000..4d48dc376a --- /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 +{ + public 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/RateLimit/LockFreeTokenBucketRateLimiterTests.cs b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs index bc299e7c47..cd307b56d8 100644 --- a/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs +++ b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs @@ -7,6 +7,7 @@ using Polly.RateLimit; using Polly.Utilities; using FluentAssertions.Execution; +using Polly.Specs.Helpers.RateLimit; using Xunit; using Xunit.Sdk; @@ -32,22 +33,14 @@ public void Given_bucket_capacity_one_and_time_not_advanced_ratelimiter_specifie TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); var rateLimiter = GetRateLimiter(onePer, 1); - // Act - first execution after initialising should always be permitted. - (bool permitExecution, TimeSpan retryAfter) canExecute = rateLimiter.PermitExecution(); - - // Assert. - canExecute.permitExecution.Should().BeTrue(); - canExecute.retryAfter.Should().Be(TimeSpan.Zero); + // Assert - first execution after initialising should always be permitted. + rateLimiter.ShouldPermitAnExecution(); // Arrange // (do nothing - time not advanced) - // Act - try another execution - canExecute = rateLimiter.PermitExecution(); - // Assert - should be blocked - time not advanced. - canExecute.permitExecution.Should().BeFalse(); - canExecute.retryAfter.Should().Be(onePer); + rateLimiter.ShouldNotPermitAnExecution(); } [Theory] @@ -63,16 +56,10 @@ public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_ex var rateLimiter = GetRateLimiter(onePer, bucketCapacity); // Act - should be able to successfully take bucketCapacity items. - AssertCanTakeNItems(rateLimiter, bucketCapacity); - - // After that, should not be able to take any items (given time not advanced). + rateLimiter.ShouldPermitNExecutions(bucketCapacity); - // Act - (bool permitExecution, TimeSpan retryAfter) canExecute = rateLimiter.PermitExecution(); - - // Assert - should be blocked - time not advanced. - canExecute.permitExecution.Should().BeFalse(); - canExecute.retryAfter.Should().BeGreaterThan(TimeSpan.Zero); + // Assert - should not be able to take any items (given time not advanced). + rateLimiter.ShouldNotPermitAnExecution(); } @@ -85,43 +72,35 @@ public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_ex [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); - FixClock(); - // Arrange - spend the initial bucket capacity. - AssertCanTakeNItems(rateLimiter, bucketCapacity); + rateLimiter.ShouldPermitNExecutions(bucketCapacity); // Act-Assert - repeatedly advance the clock towards the interval but not quite - then to the interval int experimentRepeats = bucketCapacity * 3; - const int shortfallFromInterval = 1; - long notQuiteInterval = onePer.Ticks - shortfallFromInterval; - (bool permitExecution, TimeSpan retryAfter) canExecute; + 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); - // Act - canExecute = rateLimiter.PermitExecution(); + AdvanceClock(notQuiteInterval.Ticks); + // Assert - should not quite be able to issue another token - canExecute.permitExecution.Should().BeFalse(); - canExecute.retryAfter.Should().Be(TimeSpan.FromTicks(shortfallFromInterval)); + rateLimiter.ShouldNotPermitAnExecution(shortfallFromInterval); // Arrange - Advance clock to the interval - AdvanceClock(shortfallFromInterval); - // Act - canExecute = rateLimiter.PermitExecution(); - // Assert - should be able to issue another token - canExecute.permitExecution.Should().BeTrue(); - canExecute.retryAfter.Should().Be(TimeSpan.Zero); + AdvanceClock(shortfallFromInterval.Ticks); // Act - canExecute = rateLimiter.PermitExecution(); - // Assert - cannot get another token straight away - canExecute.permitExecution.Should().BeFalse(); - canExecute.retryAfter.Should().BeGreaterThan(TimeSpan.Zero); + rateLimiter.ShouldPermitAnExecution(); + + // Assert - but cannot get another token straight away + rateLimiter.ShouldNotPermitAnExecution(); } } @@ -131,6 +110,8 @@ public void Given_any_bucket_capacity_ratelimiter_permits_another_execution_per_ [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); @@ -197,20 +178,6 @@ private static void AdvanceClock(long advanceTicks) SystemClock.DateTimeOffsetUtcNow = () => now + TimeSpan.FromTicks(advanceTicks); } - private static void AssertCanTakeNItems(IRateLimiter rateLimiter, long expectedTake) - { - (bool permitExecution, TimeSpan retryAfter) canExecute; - for (int take = 0; take < expectedTake; take++) - { - // Act - canExecute = rateLimiter.PermitExecution(); - - // Assert. - canExecute.permitExecution.Should().BeTrue(); - canExecute.retryAfter.Should().Be(TimeSpan.Zero); - } - } - public void Dispose() { SystemClock.Reset(); From 29a63fad482460cd0aba96cc9954d2ffe1f95ebd Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 06:35:33 +0100 Subject: [PATCH 07/27] Factor out common tests; add tests on lock-based rate limiter --- .../LockBasedTokenBucketRateLimiterTests.cs | 11 ++ .../LockFreeTokenBucketRateLimiterTests.cs | 180 +---------------- .../TokenBucketRateLimiterTestsBase.cs | 183 ++++++++++++++++++ 3 files changed, 196 insertions(+), 178 deletions(-) create mode 100644 src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs create mode 100644 src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs diff --git a/src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs b/src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs new file mode 100644 index 0000000000..23be20161b --- /dev/null +++ b/src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs @@ -0,0 +1,11 @@ +using System; +using Polly.RateLimit; + +namespace Polly.Specs.RateLimit +{ + public class LockBasedTokenBucketRateLimiterTests : TokenBucketRateLimiterTestsBase + { + public override IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) + => new LockBasedTokenBucketRateLimiter(onePer, bucketCapacity); + } +} diff --git a/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs index cd307b56d8..c9af7fa837 100644 --- a/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs +++ b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs @@ -1,187 +1,11 @@ using System; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentAssertions; using Polly.RateLimit; -using Polly.Utilities; -using FluentAssertions.Execution; -using Polly.Specs.Helpers.RateLimit; -using Xunit; -using Xunit.Sdk; namespace Polly.Specs.RateLimit { - - [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] - - public class LockFreeTokenBucketRateLimiterTests : IDisposable + public class LockFreeTokenBucketRateLimiterTests : TokenBucketRateLimiterTestsBase { - public IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) + public override IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); - - [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(); - } - - [Theory] - [InlineData(1)] - [InlineData(2)] - [InlineData(50)] - public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_executions_up_to_bucket_capacity(long 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(); - } - - - [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); - - // 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(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(5), () => 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); - } - - /// - /// 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. - private void Within(TimeSpan timeSpan, Action actionContainingAssertions) - { - TimeSpan retryInterval = TimeSpan.FromTicks(Math.Min(TimeSpan.FromSeconds(0.2).Ticks, timeSpan.Ticks/10)); - - 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); - } - } - } - - private static void FixClock() - { - DateTimeOffset now = DateTimeOffset.UtcNow; - SystemClock.DateTimeOffsetUtcNow = () => now; - } - - private static void AdvanceClock(long advanceTicks) - { - DateTimeOffset now = SystemClock.DateTimeOffsetUtcNow(); - SystemClock.DateTimeOffsetUtcNow = () => now + TimeSpan.FromTicks(advanceTicks); - } - - public void Dispose() - { - SystemClock.Reset(); - } - } } diff --git a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs new file mode 100644 index 0000000000..c524be88f7 --- /dev/null +++ b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs @@ -0,0 +1,183 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Execution; +using Polly.RateLimit; +using Polly.Specs.Helpers.RateLimit; +using Polly.Utilities; +using Xunit; +using Xunit.Sdk; + +namespace Polly.Specs.RateLimit +{ + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + public abstract class TokenBucketRateLimiterTestsBase : IDisposable + { + public 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(); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(50)] + public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_executions_up_to_bucket_capacity(long 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(); + } + + + [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); + + // 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(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(5), () => 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); + } + + /// + /// 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. + private void Within(TimeSpan timeSpan, Action actionContainingAssertions) + { + TimeSpan retryInterval = TimeSpan.FromTicks(Math.Min(TimeSpan.FromSeconds(0.2).Ticks, timeSpan.Ticks / 10)); + + 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); + } + } + } + + private static void FixClock() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + SystemClock.DateTimeOffsetUtcNow = () => now; + } + + private static void AdvanceClock(long advanceTicks) + { + DateTimeOffset now = SystemClock.DateTimeOffsetUtcNow(); + SystemClock.DateTimeOffsetUtcNow = () => now + TimeSpan.FromTicks(advanceTicks); + } + } +} From 6ff044845f64b7d9368cc6aa1d8202675fc8bf0b Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 06:56:41 +0100 Subject: [PATCH 08/27] Allow for slow-running on CI servers --- src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs index c524be88f7..666398a6c7 100644 --- a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs +++ b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs @@ -132,7 +132,7 @@ public void Given_immediate_parallel_contention_ratelimiter_still_only_permits_o // Act - release gate. gate.Set(); - Within(TimeSpan.FromSeconds(5), () => tasks.All(t => t.IsCompleted).Should().BeTrue()); + 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(); From ead0d14910c6ceb13c56231545b4b4ad963e81bf Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 06:57:37 +0100 Subject: [PATCH 09/27] Add tests on full bucket capacity --- .../TokenBucketRateLimiterTestsBase.cs | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs index 666398a6c7..a84a0df9d5 100644 --- a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs +++ b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs @@ -42,7 +42,7 @@ public void Given_bucket_capacity_one_and_time_not_advanced_ratelimiter_specifie // (do nothing - time not advanced) // Assert - should be blocked - time not advanced. - rateLimiter.ShouldNotPermitAnExecution(); + rateLimiter.ShouldNotPermitAnExecution(onePer); } [Theory] @@ -61,7 +61,7 @@ public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_ex rateLimiter.ShouldPermitNExecutions(bucketCapacity); // Assert - should not be able to take any items (given time not advanced). - rateLimiter.ShouldNotPermitAnExecution(); + rateLimiter.ShouldNotPermitAnExecution(onePer); } @@ -82,6 +82,7 @@ public void Given_any_bucket_capacity_ratelimiter_permits_another_execution_per_ // 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; @@ -106,6 +107,82 @@ public void Given_any_bucket_capacity_ratelimiter_permits_another_execution_per_ } } + [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)] @@ -147,7 +224,7 @@ public void Given_immediate_parallel_contention_ratelimiter_still_only_permits_o /// The action containing fluent assertions, which must succeed within the timespan. private void Within(TimeSpan timeSpan, Action actionContainingAssertions) { - TimeSpan retryInterval = TimeSpan.FromTicks(Math.Min(TimeSpan.FromSeconds(0.2).Ticks, timeSpan.Ticks / 10)); + TimeSpan retryInterval = TimeSpan.FromSeconds(0.2); Stopwatch watch = Stopwatch.StartNew(); while (true) @@ -174,10 +251,12 @@ private static void FixClock() SystemClock.DateTimeOffsetUtcNow = () => now; } - private static void AdvanceClock(long advanceTicks) + private static void AdvanceClock(TimeSpan advance) { DateTimeOffset now = SystemClock.DateTimeOffsetUtcNow(); - SystemClock.DateTimeOffsetUtcNow = () => now + TimeSpan.FromTicks(advanceTicks); + SystemClock.DateTimeOffsetUtcNow = () => now + advance; } + + private static void AdvanceClock(long advanceTicks) => AdvanceClock(TimeSpan.FromTicks(advanceTicks)); } } From ce36ab28b2c43e504d8c76b9ffc76a5fe56a6067 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 21:53:14 +0100 Subject: [PATCH 10/27] Fix RateLimitRejectedException --- src/Polly/RateLimit/RateLimitRejectedException.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Polly/RateLimit/RateLimitRejectedException.cs b/src/Polly/RateLimit/RateLimitRejectedException.cs index 3b7fbda1d8..c26f39585a 100644 --- a/src/Polly/RateLimit/RateLimitRejectedException.cs +++ b/src/Polly/RateLimit/RateLimitRejectedException.cs @@ -60,6 +60,7 @@ public RateLimitRejectedException(TimeSpan retryAfter, String message, Exception private void SetRetryAfter(TimeSpan retryAfter) { if (retryAfter < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(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}"; From 9b74ae1e6d2c69cecbc197168bfe9e2226e3959e Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 21:53:41 +0100 Subject: [PATCH 11/27] Remove unused configuration overloads --- .../RateLimit/AsyncRateLimitTResultSyntax.cs | 80 ++----------------- 1 file changed, 6 insertions(+), 74 deletions(-) diff --git a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs index 5ffdbeea04..2ba24a5c03 100644 --- a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs +++ b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs @@ -1,80 +1,12 @@ using System; -using System.Globalization; -using System.Net; -using System.Net.Http; using Polly.RateLimit; namespace Polly { public partial class Policy { - - /* Maybe these commented out overloads with TimeSpan permitOneExecutionPer are not as intuitive as the overloads left uncommented. - - /// - /// Builds a RateLimit that will rate-limit executions to one per the timespan given. - /// A will be thrown to indicate rate-limiting. - /// - /// How often one execution is permitted. - /// The type of return values this policy will handle. - /// The policy instance. - public static AsyncRateLimitPolicy RateLimitAsync(TimeSpan permitOneExecutionPer) - { - return RateLimitAsync(permitOneExecutionPer, null); - } - - /// - /// Builds a RateLimit that will rate-limit executions to one per the timespan given, - /// with a maximum burst size of . - /// A will be thrown to indicate rate-limiting. - /// - /// How often one execution is 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 type of return values this policy will handle. - /// The policy instance. - public static AsyncRateLimitPolicy RateLimitAsync(TimeSpan permitOneExecutionPer, int maxBurst) - { - return RateLimitAsync(permitOneExecutionPer, maxBurst, null); - } - - /// - /// Builds a RateLimit that will rate-limit executions to one per the timespan given. - /// - /// The type of return values this policy will handle. - /// How often one execution is permitted. - /// An (optional) factory to use to express retry-after back to the caller, when an operation is rate-limited. - /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. - /// The policy instance. - public static AsyncRateLimitPolicy RateLimitAsync( - TimeSpan permitOneExecutionPer, - Func retryAfterFactory) - { - return RateLimitAsync(permitOneExecutionPer, 1, retryAfterFactory); - } - - /// - /// Builds a RateLimit that will rate-limit executions to one per the timespan given, - /// with a maximum burst size of - /// - /// The type of return values this policy will handle. - /// How often one execution is 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. - /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. - /// The policy instance. - public static AsyncRateLimitPolicy RateLimitAsync( - TimeSpan permitOneExecutionPer, - int maxBurst, - Func retryAfterFactory) - { - return RateLimitAsync(new TokenBucketRateLimiter(permitOneExecutionPer, maxBurst), retryAfterFactory); - } - */ - private static readonly Func DefaultRateLimiterFactory = (onePer, bucketCapacity) => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); - // private readonly Func DefaultRateLimiterFactory = (onePer, bucketCapacity) => new LockBasedTokenBucketRateLimiter(onePer, bucketCapacity); + /* private readonly Func DefaultRateLimiterFactory = (onePer, bucketCapacity) => new LockBasedTokenBucketRateLimiter(onePer, bucketCapacity); */ /// /// Builds a RateLimit that will rate-limit executions to per the timespan given. @@ -96,8 +28,8 @@ public static AsyncRateLimitPolicy RateLimitAsync( /// 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 use to express retry-after back to the caller, when an operation is rate-limited. - /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. + /// 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, @@ -117,7 +49,7 @@ public static AsyncRateLimitPolicy RateLimitAsync( /// 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. - /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. + /// If null, a with property will be thrown to indicate rate-limiting. /// The policy instance. public static AsyncRateLimitPolicy RateLimitAsync( int numberOfExecutions, @@ -134,9 +66,9 @@ public static AsyncRateLimitPolicy RateLimitAsync( /// The type of return values this policy will handle. /// The rate-limiter to use to determine whether the execution is permitted. /// An (optional) factory to use to express retry-after back to the caller, when an operation is rate-limited. - /// This parameter can be null. If null, a will be thrown to indicate rate-limiting. + /// If null, a with property will be thrown to indicate rate-limiting. /// The policy instance. - public static AsyncRateLimitPolicy RateLimitAsync( + private static AsyncRateLimitPolicy RateLimitAsync( IRateLimiter rateLimiter, Func retryAfterFactory) { From 0e0d94f79d9f20e1a9a6f5df234aa1649fe2f2d4 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 22:29:57 +0100 Subject: [PATCH 12/27] Introduce a factory for obtaining the preferred rate-limiter implementation --- src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs | 3 --- src/Polly/RateLimit/RateLimiterFactory.cs | 13 +++++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/Polly/RateLimit/RateLimiterFactory.cs diff --git a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs index 2ba24a5c03..1836ec7136 100644 --- a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs +++ b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs @@ -5,9 +5,6 @@ namespace Polly { public partial class Policy { - private static readonly Func DefaultRateLimiterFactory = (onePer, bucketCapacity) => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); - /* private readonly Func DefaultRateLimiterFactory = (onePer, bucketCapacity) => new LockBasedTokenBucketRateLimiter(onePer, bucketCapacity); */ - /// /// Builds a RateLimit that will rate-limit executions to per the timespan given. /// diff --git a/src/Polly/RateLimit/RateLimiterFactory.cs b/src/Polly/RateLimit/RateLimiterFactory.cs new file mode 100644 index 0000000000..86adda09cb --- /dev/null +++ b/src/Polly/RateLimit/RateLimiterFactory.cs @@ -0,0 +1,13 @@ +using System; + +namespace Polly.RateLimit +{ + internal class RateLimiterFactory + { + public static IRateLimiter Create(TimeSpan onePer, int bucketCapacity) + => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); +/* + => new LockBasedTokenBucketRateLimiter(onePer, bucketCapacity); +*/ + } +} From 3d83844290c849415e5a0d51f694d89fb867640a Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 22:30:24 +0100 Subject: [PATCH 13/27] Pull some test helper methods into a common base-class --- .../RateLimit/RateLimitSpecsBase.cs | 54 +++++++++++++++++++ .../TokenBucketRateLimiterTestsBase.cs | 47 +--------------- 2 files changed, 55 insertions(+), 46 deletions(-) create mode 100644 src/Polly.Specs/RateLimit/RateLimitSpecsBase.cs diff --git a/src/Polly.Specs/RateLimit/RateLimitSpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitSpecsBase.cs new file mode 100644 index 0000000000..b5bcd58360 --- /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 index a84a0df9d5..7f886e5b43 100644 --- a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs +++ b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs @@ -1,20 +1,17 @@ using System; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; -using FluentAssertions.Execution; using Polly.RateLimit; using Polly.Specs.Helpers.RateLimit; using Polly.Utilities; using Xunit; -using Xunit.Sdk; namespace Polly.Specs.RateLimit { [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] - public abstract class TokenBucketRateLimiterTestsBase : IDisposable + public abstract class TokenBucketRateLimiterTestsBase : RateLimitSpecsBase, IDisposable { public abstract IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity); @@ -216,47 +213,5 @@ public void Given_immediate_parallel_contention_ratelimiter_still_only_permits_o results.Count(r => r.permitExecution).Should().Be(1); results.Count(r => !r.permitExecution).Should().Be(parallelContention - 1); } - - /// - /// 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. - private 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); - } - } - } - - private static void FixClock() - { - DateTimeOffset now = DateTimeOffset.UtcNow; - SystemClock.DateTimeOffsetUtcNow = () => now; - } - - private static void AdvanceClock(TimeSpan advance) - { - DateTimeOffset now = SystemClock.DateTimeOffsetUtcNow(); - SystemClock.DateTimeOffsetUtcNow = () => now + advance; - } - - private static void AdvanceClock(long advanceTicks) => AdvanceClock(TimeSpan.FromTicks(advanceTicks)); } } From 23f26e8e48dd873c90f6bea51301ab86e4e696e4 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Thu, 4 Jul 2019 22:30:43 +0100 Subject: [PATCH 14/27] Add first specs on async policy syntax --- .../AsyncRateLimitPolicyTResultSpecs.cs | 27 ++++++++ .../RateLimit/RateLimitPolicySpecsBase.cs | 67 +++++++++++++++++++ .../RateLimit/AsyncRateLimitTResultSyntax.cs | 30 +++++---- 3 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs create mode 100644 src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs diff --git a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs new file mode 100644 index 0000000000..b6aaebb580 --- /dev/null +++ b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs @@ -0,0 +1,27 @@ +using System; +using Polly.RateLimit; +using Polly.Specs.Helpers; +using Polly.Utilities; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] + public class AsyncRateLimitPolicyTResultSpecs : RateLimitPolicySpecsBase, IDisposable + { + public void Dispose() + { + SystemClock.Reset(); + } + + public override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) + { + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); + } + + public override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) + { + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst); + } + } +} diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs new file mode 100644 index 0000000000..7a897b9eda --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs @@ -0,0 +1,67 @@ +using System; +using FluentAssertions; +using Polly.RateLimit; +using Xunit; + +namespace Polly.Specs.RateLimit +{ + public abstract class RateLimitPolicySpecsBase + { + public abstract IRateLimitPolicy GetPolicyViaSyntax( + int numberOfExecutions, + TimeSpan perTimeSpan); + + public abstract IRateLimitPolicy GetPolicyViaSyntax( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst); + + [Fact] + public void Syntax_should_throw_for_numberOfExecutions_negative() + { + Action invalidSyntax = () => GetPolicyViaSyntax(-1, TimeSpan.FromSeconds(1)); + + invalidSyntax.ShouldThrow().And.ParamName.Should().Be("numberOfExecutions"); + } + + [Fact] + public void Syntax_should_throw_for_numberOfExecutions_zero() + { + Action invalidSyntax = () => GetPolicyViaSyntax(0, TimeSpan.FromSeconds(1)); + + invalidSyntax.ShouldThrow().And.ParamName.Should().Be("numberOfExecutions"); + } + + [Fact] + public void Syntax_should_throw_for_perTimeSpan_negative() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromTicks(-1)); + + invalidSyntax.ShouldThrow().And.ParamName.Should().Be("perTimeSpan"); + } + + [Fact] + public void Syntax_should_throw_for_perTimeSpan_zero() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.Zero); + + invalidSyntax.ShouldThrow().And.ParamName.Should().Be("perTimeSpan"); + } + + [Fact] + public void Syntax_should_throw_for_maxBurst_negative() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromSeconds(1), -1); + + invalidSyntax.ShouldThrow().And.ParamName.Should().Be("maxBurst"); + } + + [Fact] + public void Syntax_should_throw_for_maxBurst_zero() + { + Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromSeconds(1), 0); + + invalidSyntax.ShouldThrow().And.ParamName.Should().Be("maxBurst"); + } + } +} diff --git a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs index 1836ec7136..4dd9773223 100644 --- a/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs +++ b/src/Polly/RateLimit/AsyncRateLimitTResultSyntax.cs @@ -37,39 +37,45 @@ public static AsyncRateLimitPolicy RateLimitAsync( } /// - /// Builds a RateLimit that will rate-limit executions to per the timespan given, - /// with a maximum burst size of + /// 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. - /// 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) + int maxBurst) { - return RateLimitAsync(DefaultRateLimiterFactory(TimeSpan.FromTicks(perTimeSpan.Ticks / numberOfExecutions), maxBurst), retryAfterFactory); + return RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst, null); } /// - /// Builds a RateLimit that will rate-limit executions with the provided . + /// 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 rate-limiter to use to determine whether the execution is permitted. + /// 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. - private static AsyncRateLimitPolicy RateLimitAsync( - IRateLimiter rateLimiter, + public static AsyncRateLimitPolicy RateLimitAsync( + int numberOfExecutions, + TimeSpan perTimeSpan, + int maxBurst, Func retryAfterFactory) { - if (rateLimiter == null) throw new NullReferenceException(nameof(rateLimiter)); + if (numberOfExecutions < 1) throw new ArgumentOutOfRangeException(nameof(numberOfExecutions), $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(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); } From d0f191bb0620700c4cd368ee302617ab71cc29c8 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Fri, 5 Jul 2019 08:17:20 +0100 Subject: [PATCH 15/27] Add full set of specs on rate-limit policies thus far --- .../AsyncRateLimitPolicyTResultSpecs.cs | 25 +- .../RateLimit/RateLimitPolicySpecsBase.cs | 251 +++++++++++++++++- .../TokenBucketRateLimiterTestsBase.cs | 3 +- 3 files changed, 264 insertions(+), 15 deletions(-) diff --git a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs index b6aaebb580..3c82731998 100644 --- a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs +++ b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Polly.RateLimit; using Polly.Specs.Helpers; using Polly.Utilities; @@ -14,14 +15,34 @@ public void Dispose() SystemClock.Reset(); } - public override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) + protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) { return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); } - public override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) + 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 ResultClass(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/RateLimitPolicySpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs index 7a897b9eda..261d8dbb7c 100644 --- a/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs +++ b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs @@ -1,21 +1,65 @@ 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 + public abstract class RateLimitPolicySpecsBase : RateLimitSpecsBase { - public abstract IRateLimitPolicy GetPolicyViaSyntax( + protected abstract IRateLimitPolicy GetPolicyViaSyntax( int numberOfExecutions, TimeSpan perTimeSpan); - public abstract IRateLimitPolicy GetPolicyViaSyntax( + 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) canExecute = TryExecuteThroughPolicy(policy); + + canExecute.permitExecution.Should().BeTrue(); + canExecute.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.ShouldThrow().And.ParamName.Should().Be("perTimeSpan"); + } + [Fact] public void Syntax_should_throw_for_numberOfExecutions_negative() { @@ -40,14 +84,6 @@ public void Syntax_should_throw_for_perTimeSpan_negative() invalidSyntax.ShouldThrow().And.ParamName.Should().Be("perTimeSpan"); } - [Fact] - public void Syntax_should_throw_for_perTimeSpan_zero() - { - Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.Zero); - - invalidSyntax.ShouldThrow().And.ParamName.Should().Be("perTimeSpan"); - } - [Fact] public void Syntax_should_throw_for_maxBurst_negative() { @@ -63,5 +99,198 @@ public void Syntax_should_throw_for_maxBurst_zero() invalidSyntax.ShouldThrow().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 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 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/TokenBucketRateLimiterTestsBase.cs b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs index 7f886e5b43..0eefc960fa 100644 --- a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs +++ b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs @@ -46,7 +46,7 @@ public void Given_bucket_capacity_one_and_time_not_advanced_ratelimiter_specifie [InlineData(1)] [InlineData(2)] [InlineData(50)] - public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_executions_up_to_bucket_capacity(long bucketCapacity) + public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_executions_up_to_bucket_capacity(int bucketCapacity) { FixClock(); @@ -61,7 +61,6 @@ public void Given_bucket_capacity_N_and_time_not_advanced_ratelimiter_permits_ex rateLimiter.ShouldNotPermitAnExecution(onePer); } - [Theory] [InlineData(1, 1)] [InlineData(2, 1)] From 705cda98ba4faab3b9e84571d1b115f47a489feb Mon Sep 17 00:00:00 2001 From: reisenberger Date: Fri, 5 Jul 2019 08:42:54 +0100 Subject: [PATCH 16/27] Add tests on retryAfterFactory --- .../RateLimit/ResultClassWithRetryAfter.cs | 21 +++++++++ .../AsyncRateLimitPolicyTResultSpecs.cs | 29 ++++++++++-- .../RateLimitPolicyTResultSpecsBase.cs | 47 +++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 src/Polly.Specs/Helpers/RateLimit/ResultClassWithRetryAfter.cs create mode 100644 src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs diff --git a/src/Polly.Specs/Helpers/RateLimit/ResultClassWithRetryAfter.cs b/src/Polly.Specs/Helpers/RateLimit/ResultClassWithRetryAfter.cs new file mode 100644 index 0000000000..fcb1c72c46 --- /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/AsyncRateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs index 3c82731998..263d11a412 100644 --- a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs +++ b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs @@ -2,13 +2,14 @@ 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 : RateLimitPolicySpecsBase, IDisposable + public class AsyncRateLimitPolicyTResultSpecs : RateLimitPolicyTResultSpecsBase, IDisposable { public void Dispose() { @@ -17,21 +18,27 @@ public void Dispose() protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) { - return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); } protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) { - return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan, 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) + if (policy is AsyncRateLimitPolicy typedPolicy) { try { - typedPolicy.ExecuteAsync(() => Task.FromResult(new ResultClass(ResultPrimitive.Good))).GetAwaiter().GetResult(); + typedPolicy.ExecuteAsync(() => Task.FromResult(new ResultClassWithRetryAfter(ResultPrimitive.Good))).GetAwaiter().GetResult(); return (true, TimeSpan.Zero); } catch (RateLimitRejectedException e) @@ -44,5 +51,17 @@ protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy pol throw new InvalidOperationException("Unexpected policy type in test construction."); } } + + protected override TResult TryExecuteThroughPolicy(IRateLimitPolicy policy, TResult resultIfExecutionPermitted) + { + if (policy is AsyncRateLimitPolicy typedPolicy) + { + return typedPolicy.ExecuteAsync(() => Task.FromResult(resultIfExecutionPermitted)).GetAwaiter().GetResult(); + } + 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 0000000000..7d486c680f --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs @@ -0,0 +1,47 @@ +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, TResult resultIfExecutionPermitted); + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(5)] + public void Ratelimiter_specifies_correct_wait_until_next_execution_by_custom_factory(int onePerSeconds) + { + FixClock(); + + // Arrange + TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); + Func retryAfterFactory = (t, ctx) => new ResultClassWithRetryAfter(t); + var rateLimiter = GetPolicyViaSyntax(1, onePer, 1, retryAfterFactory); + + // Assert - first execution after initialising should always be permitted. + ShouldPermitAnExecution(rateLimiter); + + // Arrange + // (do nothing - time not advanced) + + // Act - try another execution. + var resultExpectedBlocked = TryExecuteThroughPolicy(rateLimiter, new ResultClassWithRetryAfter(ResultPrimitive.Good)); + + // Assert - should be blocked - time not advanced. Result should be expressed per the retryAfterFactory. + resultExpectedBlocked.ResultCode.Should().NotBe(ResultPrimitive.Good); + resultExpectedBlocked.RetryAfter.Should().Be(onePer); + } + } +} From 68346c73c7136dc6dd403f8821d1c30ad2382785 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Fri, 5 Jul 2019 08:50:59 +0100 Subject: [PATCH 17/27] Add tests on context passed to retryAfterFactory --- .../AsyncRateLimitPolicyTResultSpecs.cs | 4 ++-- .../RateLimitPolicyTResultSpecsBase.cs | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs index 263d11a412..19073d3f3e 100644 --- a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs +++ b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicyTResultSpecs.cs @@ -52,11 +52,11 @@ protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy pol } } - protected override TResult TryExecuteThroughPolicy(IRateLimitPolicy policy, TResult resultIfExecutionPermitted) + protected override TResult TryExecuteThroughPolicy(IRateLimitPolicy policy, Context context, TResult resultIfExecutionPermitted) { if (policy is AsyncRateLimitPolicy typedPolicy) { - return typedPolicy.ExecuteAsync(() => Task.FromResult(resultIfExecutionPermitted)).GetAwaiter().GetResult(); + return typedPolicy.ExecuteAsync(ctx => Task.FromResult(resultIfExecutionPermitted), context).GetAwaiter().GetResult(); } else { diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs index 7d486c680f..3fc16eb21e 100644 --- a/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs +++ b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecsBase.cs @@ -15,33 +15,43 @@ protected abstract IRateLimitPolicy GetPolicyViaSyntax( int maxBurst, Func retryAfterFactory); - protected abstract TResult TryExecuteThroughPolicy(IRateLimitPolicy policy, TResult resultIfExecutionPermitted); + 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(int onePerSeconds) + public void Ratelimiter_specifies_correct_wait_until_next_execution_by_custom_factory_passing_correct_context(int onePerSeconds) { FixClock(); // Arrange TimeSpan onePer = TimeSpan.FromSeconds(onePerSeconds); - Func retryAfterFactory = (t, ctx) => new ResultClassWithRetryAfter(t); + Context contextPassedToRetryAfter = null; + Func retryAfterFactory = (t, ctx) => + { + contextPassedToRetryAfter = ctx; + return new ResultClassWithRetryAfter(t); + }; var rateLimiter = GetPolicyViaSyntax(1, onePer, 1, retryAfterFactory); - // Assert - first execution after initialising should always be permitted. + // Arrange - drain first permitted execution after initialising. ShouldPermitAnExecution(rateLimiter); // Arrange // (do nothing - time not advanced) // Act - try another execution. - var resultExpectedBlocked = TryExecuteThroughPolicy(rateLimiter, new ResultClassWithRetryAfter(ResultPrimitive.Good)); + Context contextToPassIn = new Context(); + var resultExpectedBlocked = TryExecuteThroughPolicy(rateLimiter, contextToPassIn, new ResultClassWithRetryAfter(ResultPrimitive.Good)); - // Assert - should be blocked - time not advanced. Result should be expressed per the retryAfterFactory. + // 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); } } } From 1895edc0a116e37f69157c7e6ee3b037b68c428a Mon Sep 17 00:00:00 2001 From: reisenberger Date: Fri, 5 Jul 2019 20:52:27 +0100 Subject: [PATCH 18/27] Add async non-generic syntax and specs --- .../RateLimit/AsyncRateLimitPolicySpecs.cs | 49 +++++++++++++++++++ src/Polly/RateLimit/AsyncRateLimitSyntax.cs | 43 ++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs create mode 100644 src/Polly/RateLimit/AsyncRateLimitSyntax.cs diff --git a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs new file mode 100644 index 0000000000..7bc7bacf2d --- /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/RateLimit/AsyncRateLimitSyntax.cs b/src/Polly/RateLimit/AsyncRateLimitSyntax.cs new file mode 100644 index 0000000000..b3532c4f91 --- /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), $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(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); + } + } +} From dd10d3e36161dcbe562aa918ec6e37d4aa1829a0 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Fri, 5 Jul 2019 21:11:24 +0100 Subject: [PATCH 19/27] Improve code layout --- src/Polly/RateLimit/AsyncRateLimitEngine.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Polly/RateLimit/AsyncRateLimitEngine.cs b/src/Polly/RateLimit/AsyncRateLimitEngine.cs index 703eb56837..16f40caec7 100644 --- a/src/Polly/RateLimit/AsyncRateLimitEngine.cs +++ b/src/Polly/RateLimit/AsyncRateLimitEngine.cs @@ -10,7 +10,9 @@ internal static async Task ImplementationAsync( IRateLimiter rateLimiter, Func retryAfterFactory, Func> action, - Context context, CancellationToken cancellationToken, bool continueOnCapturedContext + Context context, + CancellationToken cancellationToken, + bool continueOnCapturedContext ) { (bool permit, TimeSpan retryAfter) = rateLimiter.PermitExecution(); From a734da2948a3cde428fe0802c58e339865cfd611 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Fri, 5 Jul 2019 21:12:07 +0100 Subject: [PATCH 20/27] Add sync rate-limit policies --- .../RateLimit/RateLimitPolicySpecs.cs | 48 +++++++++++ .../RateLimit/RateLimitPolicyTResultSpecs.cs | 67 +++++++++++++++ src/Polly/RateLimit/RateLimitEngine.cs | 31 +++++++ src/Polly/RateLimit/RateLimitPolicy.cs | 46 ++++++++++ src/Polly/RateLimit/RateLimitSyntax.cs | 43 ++++++++++ src/Polly/RateLimit/RateLimitTResultSyntax.cs | 83 +++++++++++++++++++ 6 files changed, 318 insertions(+) create mode 100644 src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs create mode 100644 src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs create mode 100644 src/Polly/RateLimit/RateLimitEngine.cs create mode 100644 src/Polly/RateLimit/RateLimitPolicy.cs create mode 100644 src/Polly/RateLimit/RateLimitSyntax.cs create mode 100644 src/Polly/RateLimit/RateLimitTResultSyntax.cs diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs b/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs new file mode 100644 index 0000000000..e8641e8745 --- /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/RateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs new file mode 100644 index 0000000000..76d90deb79 --- /dev/null +++ b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.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 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/RateLimit/RateLimitEngine.cs b/src/Polly/RateLimit/RateLimitEngine.cs new file mode 100644 index 0000000000..a96cbb2835 --- /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 0000000000..a338c0ecd9 --- /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 NullReferenceException(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 NullReferenceException(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/RateLimitSyntax.cs b/src/Polly/RateLimit/RateLimitSyntax.cs new file mode 100644 index 0000000000..5d16f9103f --- /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), $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(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 0000000000..9314ffe21d --- /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), $"{nameof(numberOfExecutions)} per timespan must be an integer greater than or equal to 1."); + if (perTimeSpan <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(perTimeSpan), $"{nameof(perTimeSpan)} must be a positive timespan."); + if (maxBurst < 1) throw new ArgumentOutOfRangeException(nameof(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); + } + } +} From ba96aac12cb22fd98b42437eea96915a2add4af9 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Tue, 9 Jul 2019 21:51:44 +0100 Subject: [PATCH 21/27] Add initial rate-limit doco; bump to v7.3.0 --- CHANGELOG.md | 3 +++ GitVersionConfig.yaml | 2 +- README.md | 9 ++++++++- src/Polly/Polly.csproj | 10 +++++----- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf2cfeb2c..e9112cdb22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 7.3.0 + - Add RateLimit policy + ## 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 f94af5baaa..0105503e46 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 05422714db..1d947fc56b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. @@ -30,6 +30,7 @@ Polly offers multiple resilience policies: |**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. | +|**Rate-limit**
([quickstart](#ratelimit) ; [deep](https://github.com/App-vNext/Polly/wiki/RateLimit))|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. | @@ -635,6 +636,12 @@ Bulkhead policies throw `BulkheadRejectedException` if items are queued to the b For more detail see: [Bulkhead policy documentation](https://github.com/App-vNext/Polly/wiki/Bulkhead) on wiki. +### Rate-Limit + +**TODO: Documentation to be completed** + +
+ ### Cache ```csharp diff --git a/src/Polly/Polly.csproj b/src/Polly/Polly.csproj index c147c0ff41..4161a9e5d0 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 From f237d8fb626fde21c88c76339f74d079cb71c727 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Tue, 9 Jul 2019 21:51:58 +0100 Subject: [PATCH 22/27] Improve bulkhead doco in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d947fc56b..bd1d274476 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Polly offers multiple resilience policies: |**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. | +|**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](#ratelimit) ; [deep](https://github.com/App-vNext/Polly/wiki/RateLimit))|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. | From b41cac4016df3fd6e152cc9db412a73da98181d4 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Mon, 15 Jul 2019 21:50:00 +0100 Subject: [PATCH 23/27] Minor expressivity refinements --- src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs | 9 ++++++--- src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs index 8c299a8ed5..6ea0dfa6ef 100644 --- a/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs +++ b/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs @@ -65,11 +65,14 @@ public LockBasedTokenBucketRateLimiter(TimeSpan onePer, long bucketCapacity) // 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 = -ticksTillAddNextToken / addTokenTickInterval; - // For clarity: it's time to add 1 + tokensMissedAdding tokens to the bucket. + 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, 1 + tokensMissedAdding); + long tokensToAdd = Math.Min(bucketCapacity, tokensMissedAdding); // Work out when tokens would next be due to be added, if we add these tokens. long newAddNextTokenAtTicks = addNextTokenAtTicks + tokensToAdd * addTokenTickInterval; diff --git a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs index 9997725789..152fb60b43 100644 --- a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs +++ b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs @@ -69,11 +69,14 @@ public LockFreeTokenBucketRateLimiter(TimeSpan onePer, long bucketCapacity) // 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 = -ticksTillAddNextToken / addTokenTickInterval; - // For clarity: it's time to add 1 + tokensMissedAdding tokens to the bucket. + 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, 1 + tokensMissedAdding); + 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; From 1370cdb487f485c9f0c4970916d8d0816e7c3212 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Tue, 16 Jul 2019 22:04:20 +0100 Subject: [PATCH 24/27] Neaten bulkhead tests commentary --- src/Polly.Specs/Helpers/Bulkhead/TraceableAction.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Polly.Specs/Helpers/Bulkhead/TraceableAction.cs b/src/Polly.Specs/Helpers/Bulkhead/TraceableAction.cs index 97372ff0ad..3fa0fb8574 100644 --- a/src/Polly.Specs/Helpers/Bulkhead/TraceableAction.cs +++ b/src/Polly.Specs/Helpers/Bulkhead/TraceableAction.cs @@ -45,6 +45,11 @@ public void SignalStateChange() _statusChanged.Set(); } + // Note re TaskCreationOptions.LongRunning: Testing the parallelization of the bulkhead policy efficiently requires the ability to start large numbers of parallel tasks in a short space of time. + // The ThreadPool's algorithm of only injecting extra threads (when necessary) at a rate of two-per-second however makes high-volume tests using the ThreadPool both slow and flaky. In the days of PCL, further, ThreadPool.SetMinThreads(...) was not available to mitigate this. + // Using TaskCreationOptions.LongRunning allows us to force tasks to be started near-instantly on non-ThreadPool threads. + // Similarly, we use ConfigureAwait(true) when awaiting, to avoid continuations being scheduled onto a ThreadPool thread, which may only be injected too slowly in high-volume tests. + public Task ExecuteOnBulkhead(BulkheadPolicy bulkhead) { return ExecuteThroughSyncBulkheadOuter( From b890f01d6d904e96e15f4652d3ed34542cc5a660 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Tue, 16 Jul 2019 22:05:56 +0100 Subject: [PATCH 25/27] Control visibility of IRateLimiter components --- src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs | 2 +- .../RateLimit/LockBasedTokenBucketRateLimiterTests.cs | 2 +- .../RateLimit/LockFreeTokenBucketRateLimiterTests.cs | 2 +- src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs | 2 +- src/Polly/RateLimit/IRateLimiter.cs | 2 +- src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs | 2 +- src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs b/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs index 4d48dc376a..64da096567 100644 --- a/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs +++ b/src/Polly.Specs/Helpers/RateLimit/IRateLimiterExtensions.cs @@ -4,7 +4,7 @@ namespace Polly.Specs.Helpers.RateLimit { - public static class IRateLimiterExtensions + internal static class IRateLimiterExtensions { public static void ShouldPermitAnExecution(this IRateLimiter rateLimiter) { diff --git a/src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs b/src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs index 23be20161b..775f47361f 100644 --- a/src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs +++ b/src/Polly.Specs/RateLimit/LockBasedTokenBucketRateLimiterTests.cs @@ -5,7 +5,7 @@ namespace Polly.Specs.RateLimit { public class LockBasedTokenBucketRateLimiterTests : TokenBucketRateLimiterTestsBase { - public override IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) + internal override IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) => new LockBasedTokenBucketRateLimiter(onePer, bucketCapacity); } } diff --git a/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs index c9af7fa837..31376594f6 100644 --- a/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs +++ b/src/Polly.Specs/RateLimit/LockFreeTokenBucketRateLimiterTests.cs @@ -5,7 +5,7 @@ namespace Polly.Specs.RateLimit { public class LockFreeTokenBucketRateLimiterTests : TokenBucketRateLimiterTestsBase { - public override IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) + internal override IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity) => new LockFreeTokenBucketRateLimiter(onePer, bucketCapacity); } } diff --git a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs index 0eefc960fa..c296f0d5bf 100644 --- a/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs +++ b/src/Polly.Specs/RateLimit/TokenBucketRateLimiterTestsBase.cs @@ -13,7 +13,7 @@ namespace Polly.Specs.RateLimit [Collection(Polly.Specs.Helpers.Constants.SystemClockDependentTestCollection)] public abstract class TokenBucketRateLimiterTestsBase : RateLimitSpecsBase, IDisposable { - public abstract IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity); + internal abstract IRateLimiter GetRateLimiter(TimeSpan onePer, long bucketCapacity); public void Dispose() { diff --git a/src/Polly/RateLimit/IRateLimiter.cs b/src/Polly/RateLimit/IRateLimiter.cs index 9136bbcc20..79e724f3ac 100644 --- a/src/Polly/RateLimit/IRateLimiter.cs +++ b/src/Polly/RateLimit/IRateLimiter.cs @@ -5,7 +5,7 @@ namespace Polly.RateLimit /// /// Defines methods to be provided by a rate-limiter used in a Polly /// - public interface IRateLimiter + internal interface IRateLimiter { /// /// Returns whether the execution is permitted; if not, returns what should be waited before retrying. diff --git a/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs index 6ea0dfa6ef..26feb3d05c 100644 --- a/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs +++ b/src/Polly/RateLimit/LockBasedTokenBucketRateLimiter.cs @@ -6,7 +6,7 @@ namespace Polly.RateLimit /// /// A lock-based token-bucket rate-limiter for a Polly . /// - public class LockBasedTokenBucketRateLimiter : IRateLimiter + internal class LockBasedTokenBucketRateLimiter : IRateLimiter { private readonly long addTokenTickInterval; private readonly long bucketCapacity; diff --git a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs index 152fb60b43..42571d735b 100644 --- a/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs +++ b/src/Polly/RateLimit/LockFreeTokenBucketRateLimiter.cs @@ -7,7 +7,7 @@ namespace Polly.RateLimit /// /// A lock-free token-bucket rate-limiter for a Polly . /// - public class LockFreeTokenBucketRateLimiter : IRateLimiter + internal class LockFreeTokenBucketRateLimiter : IRateLimiter { private readonly long addTokenTickInterval; private readonly long bucketCapacity; From f23259543ea4778f30152c271d2c46c594b6b364 Mon Sep 17 00:00:00 2001 From: reisenberger Date: Tue, 16 Jul 2019 22:07:24 +0100 Subject: [PATCH 26/27] Fix non-generic rate-limit tests to be genuinely non-generic --- src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs | 6 +++--- src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs | 6 +++--- src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs index 7bc7bacf2d..24b70fa2e5 100644 --- a/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs +++ b/src/Polly.Specs/RateLimit/AsyncRateLimitPolicySpecs.cs @@ -18,17 +18,17 @@ public void Dispose() protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) { - return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan); } protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) { - return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst); + return Policy.RateLimitAsync(numberOfExecutions, perTimeSpan, maxBurst); } protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy policy) { - if (policy is AsyncRateLimitPolicy typedPolicy) + if (policy is AsyncRateLimitPolicy typedPolicy) { try { diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs b/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs index e8641e8745..dec7aa35f0 100644 --- a/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs +++ b/src/Polly.Specs/RateLimit/RateLimitPolicySpecs.cs @@ -17,17 +17,17 @@ public void Dispose() protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan) { - return Policy.RateLimit(numberOfExecutions, perTimeSpan); + return Policy.RateLimit(numberOfExecutions, perTimeSpan); } protected override IRateLimitPolicy GetPolicyViaSyntax(int numberOfExecutions, TimeSpan perTimeSpan, int maxBurst) { - return Policy.RateLimit(numberOfExecutions, perTimeSpan, maxBurst); + return Policy.RateLimit(numberOfExecutions, perTimeSpan, maxBurst); } protected override (bool, TimeSpan) TryExecuteThroughPolicy(IRateLimitPolicy policy) { - if (policy is RateLimitPolicy typedPolicy) + if (policy is RateLimitPolicy typedPolicy) { try { diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs index 76d90deb79..e67df0c2fc 100644 --- a/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs +++ b/src/Polly.Specs/RateLimit/RateLimitPolicyTResultSpecs.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Polly.RateLimit; using Polly.Specs.Helpers; using Polly.Specs.Helpers.RateLimit; From e566ea9c46c9e1c7943c94d7296773f5760a2dcd Mon Sep 17 00:00:00 2001 From: Yarek Tyshchenko Date: Fri, 15 Oct 2021 11:36:49 +0100 Subject: [PATCH 27/27] Fix ShouldThrow syntax errors in specs --- .../RateLimit/RateLimitPolicySpecsBase.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs index 261d8dbb7c..270bab466a 100644 --- a/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs +++ b/src/Polly.Specs/RateLimit/RateLimitPolicySpecsBase.cs @@ -57,7 +57,7 @@ public void Syntax_should_throw_for_perTimeSpan_zero() { Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.Zero); - invalidSyntax.ShouldThrow().And.ParamName.Should().Be("perTimeSpan"); + invalidSyntax.Should().Throw().And.ParamName.Should().Be("perTimeSpan"); } [Fact] @@ -65,7 +65,7 @@ public void Syntax_should_throw_for_numberOfExecutions_negative() { Action invalidSyntax = () => GetPolicyViaSyntax(-1, TimeSpan.FromSeconds(1)); - invalidSyntax.ShouldThrow().And.ParamName.Should().Be("numberOfExecutions"); + invalidSyntax.Should().Throw().And.ParamName.Should().Be("numberOfExecutions"); } [Fact] @@ -73,7 +73,7 @@ public void Syntax_should_throw_for_numberOfExecutions_zero() { Action invalidSyntax = () => GetPolicyViaSyntax(0, TimeSpan.FromSeconds(1)); - invalidSyntax.ShouldThrow().And.ParamName.Should().Be("numberOfExecutions"); + invalidSyntax.Should().Throw().And.ParamName.Should().Be("numberOfExecutions"); } [Fact] @@ -81,7 +81,7 @@ public void Syntax_should_throw_for_perTimeSpan_negative() { Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromTicks(-1)); - invalidSyntax.ShouldThrow().And.ParamName.Should().Be("perTimeSpan"); + invalidSyntax.Should().Throw().And.ParamName.Should().Be("perTimeSpan"); } [Fact] @@ -89,7 +89,7 @@ public void Syntax_should_throw_for_maxBurst_negative() { Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromSeconds(1), -1); - invalidSyntax.ShouldThrow().And.ParamName.Should().Be("maxBurst"); + invalidSyntax.Should().Throw().And.ParamName.Should().Be("maxBurst"); } [Fact] @@ -97,7 +97,7 @@ public void Syntax_should_throw_for_maxBurst_zero() { Action invalidSyntax = () => GetPolicyViaSyntax(1, TimeSpan.FromSeconds(1), 0); - invalidSyntax.ShouldThrow().And.ParamName.Should().Be("maxBurst"); + invalidSyntax.Should().Throw().And.ParamName.Should().Be("maxBurst"); } [Theory]