public async void ShouldNotThrottleOnFirstCallWithAllowedCallRateOf2PerMinute() { var utcDateTime = DateTime.UtcNow; var clockMock = GetClockMock(utcDateTime); var cacheKey = new RateLimitCacheKey("testclient_01", "GET", "localhost", "/api/values", new AllowedConsumptionRate(2, RateLimitUnit.PerMinute), _ => RateLimitUnit.PerMinute.ToString(), clockMock.Object); var setup = Arrange("testclient_01", "GET", "/api/values", new AllowedConsumptionRate(2, RateLimitUnit.PerMinute), utcDateTime, 1); var result = await setup.RedisRateLimiter.LimitRequestAsync("testclient_01", "GET", "localhost", "/api/values", new List <AllowedConsumptionRate>() { new AllowedConsumptionRate(2, RateLimitUnit.PerMinute) }, 1).ConfigureAwait(false); Assert.Equal(false, result.Throttled); Assert.Equal(0, result.WaitingIntervalInTicks); Assert.Equal(1, result.TokensRemaining); Assert.Equal(cacheKey.ToString(), result.CacheKey.ToString()); setup.ConnectionMultiplexerMock.VerifyAll(); setup.DatabaseMock.VerifyAll(); setup.TransactionMock.VerifyAll(); setup.PostViolationTransactionMock.Verify(redisTransaction => redisTransaction.SortedSetRangeByRankWithScoresAsync( cacheKey.ToString(), 0, 0, It.IsAny <Order>(), It.IsAny <CommandFlags>()), Times.Never); setup.clockMock.VerifyAll(); }
protected override Func <long> GetNumberOfRequestsAsync(string requestId, string method, string host, string routeTemplate, AllowedConsumptionRate allowedCallRate, IList <RateLimitCacheKey> cacheKeys, ITransaction redisTransaction, long utcNowTicks, int costPerCall = 1) { if (costPerCall != 1) { throw new ArgumentOutOfRangeException("Only cost of value 1 is currently supported by the sliding time window rate limiter"); } if (allowedCallRate.MaxBurst != 0) { throw new NotSupportedException("MaxBurst is not supported by the sliding time window rate limiter. Please consider using the leaky bucket rate limiter."); } var cacheKey = new RateLimitCacheKey(requestId, method, host, routeTemplate, allowedCallRate, RateLimitTypeCacheKeyFormatMapping[allowedCallRate.Unit].Invoke(allowedCallRate)); var cacheKeyString = cacheKey.ToString(); cacheKeys.Add(cacheKey); var sortedSetRemoveRangeByScoreAsync = redisTransaction.SortedSetRemoveRangeByScoreAsync( cacheKeyString, 0, utcNowTicks - GetTicksPerUnit(cacheKey.AllowedConsumptionRate)); var sortedSetAddAsync = redisTransaction.SortedSetAddAsync(cacheKeyString, Guid.NewGuid().ToString(), utcNowTicks); var numberOfRequestsInWindowAsyncList = redisTransaction.SortedSetLengthAsync(cacheKeyString); var expireTask = redisTransaction.KeyExpireAsync(cacheKeyString, cacheKey.Expiration.Add(new TimeSpan(0, 1, 0))); return(() => numberOfRequestsInWindowAsyncList.Result); }
private long GetWaitingIntervalInTicks(Func <long> getOldestRequestTimestampInTicksFunc, RateLimitCacheKey violatedCacheKey, long utcNowTicks) { return(getOldestRequestTimestampInTicksFunc() + GetTicksPerUnit(violatedCacheKey.AllowedConsumptionRate) - utcNowTicks); }
public async void ShouldThrottleOnThirdCallWithAllowedCallRateOf2PerMinute() { var utcDateTime = DateTime.UtcNow; var clockMock = GetClockMock(utcDateTime); var cacheKey = new RateLimitCacheKey("testclient_01", "GET", "localhost", "/api/values", new AllowedConsumptionRate(2, RateLimitUnit.PerMinute), _ => RateLimitUnit.PerMinute.ToString(), clockMock.Object); var setup = Arrange("testclient_01", "GET", "/api/values", new AllowedConsumptionRate(2, RateLimitUnit.PerMinute), utcDateTime, 3); var result = await setup.RedisRateLimiter.LimitRequestAsync("testclient_01", "GET", "localhost", "/api/values", new List <AllowedConsumptionRate>() { new AllowedConsumptionRate(2, RateLimitUnit.PerMinute) }).ConfigureAwait(false); Assert.Equal(result.State, ResultState.Throttled); Assert.Equal(cacheKey.ToString(), result.CacheKey.ToString()); setup.ConnectionMultiplexerMock.VerifyAll(); setup.DatabaseMock.VerifyAll(); setup.TransactionMock.VerifyAll(); setup.PostViolationTransactionMock.VerifyAll(); setup.clockMock.VerifyAll(); }
public Task <RateLimitingResult> LimitRequestAsync(RateLimitCacheKey cacheKey) { return(LimitRequestAsync(cacheKey.RequestId, cacheKey.Method, cacheKey.Host, cacheKey.RouteTemplate, new List <AllowedConsumptionRate>() { new AllowedConsumptionRate(cacheKey.Limit, cacheKey.Unit) }, 1)); }
/// <summary> /// /// </summary> /// <param name="cacheKey"></param> /// <param name="throttled"></param> /// <param name="waitingIntervalInTicks"></param> /// <param name="callsRemaining"></param> /// <param name="violatedPolicyName"></param> public RateLimitingResult(ResultState state, long waitingIntervalInTicks, RateLimitCacheKey cacheKey, int callsRemaining, string violatedPolicyName = "") { State = state; WaitingIntervalInTicks = waitingIntervalInTicks; CacheKey = cacheKey; TokensRemaining = callsRemaining; ViolatedPolicyName = violatedPolicyName; }
protected override Func <long> GetOldestRequestTimestampInTicksFunc(ITransaction postViolationTransaction, RateLimitCacheKey cacheKey, long utcNowTicks) { var task = postViolationTransaction.SortedSetRangeByRankWithScoresAsync(cacheKey.ToString(), 0, 0); return(() => { var sortedSetEntries = task.Result; if (sortedSetEntries == null || sortedSetEntries.Length == 0) { return utcNowTicks - GetTicksPerUnit(cacheKey.AllowedConsumptionRate); } return (long)sortedSetEntries[0].Score; }); }
protected override Func <long> GetNumberOfRequestsAsync( string requestId, string method, string host, string routeTemplate, AllowedConsumptionRate allowedConsumptionRate, IList <RateLimitCacheKey> cacheKeys, ITransaction redisTransaction, long utcNowTicks, int costPerCall = 1) { RateLimitCacheKey cacheKey = new RateLimitCacheKey(requestId, method, host, routeTemplate, allowedConsumptionRate, RateLimitTypeCacheKeyFormatMapping[allowedConsumptionRate.Unit].Invoke(allowedConsumptionRate)); var cacheKeyString = cacheKey.ToString(); cacheKeys.Add(cacheKey); var ttlInSeconds = allowedConsumptionRate.MaxBurst / allowedConsumptionRate.Limit * (long)allowedConsumptionRate.Unit / TimeSpan.TicksPerSecond + 120; var scriptResultTask = redisTransaction.ScriptEvaluateAsync(_loadedLuaScript, new { key = (RedisKey)cacheKeyString, utcNowTicks = utcNowTicks, leakageAmountPerInterval = allowedConsumptionRate.Limit, leakageIntervalInTicks = GetTicksPerUnit(allowedConsumptionRate), costPerCall = costPerCall, ttl = ttlInSeconds }); return(() => { return Convert.ToInt64(scriptResultTask.Result.ToString()); }); }
protected override void UndoUnsuccessfulRequestCount(ITransaction postViolationTransaction, RateLimitCacheKey cacheKey, long now, int costPerCall = 1) { if (costPerCall != 1) { throw new NotSupportedException("Only cost of value 1 is currently supported by the sliding window rate limiter"); } var adjust = postViolationTransaction.SortedSetRemoveRangeByRankAsync(cacheKey.ToString(), -1, -1); }
protected abstract Func <long> GetOldestRequestTimestampInTicksFunc( ITransaction postViolationTransaction, RateLimitCacheKey cacheKey, long utcNowTicks);
protected abstract void UndoUnsuccessfulRequestCount( ITransaction postViolationTransaction, RateLimitCacheKey cacheKey, long utcNowTicks, int costPerCall = 1);
protected override void UndoUnsuccessfulRequestCount(ITransaction postViolationTransaction, RateLimitCacheKey cacheKey, long utcNowTicks, int costPerCall = 1) { postViolationTransaction.HashDecrementAsync(cacheKey.ToString(), "t", costPerCall); }
protected override Func <long> GetOldestRequestTimestampInTicksFunc(ITransaction postViolationTransaction, RateLimitCacheKey cacheKey, long utcNowTicks) { var task = postViolationTransaction.HashGetAsync(cacheKey.ToString(), "lu"); return(() => { task.Result.TryParse(out double value); return (long)value; }); }
Arrange(string requestId, string method, string routeTemplate, AllowedConsumptionRate allowedCallRate, DateTime utcDateTime, long numberOfRequestsMadeSoFar) { var clockMock = GetClockMock(utcDateTime); var cacheKey = new RateLimitCacheKey(requestId, method, "localhost", routeTemplate, allowedCallRate, _ => allowedCallRate.Unit.ToString(), clockMock.Object); var connectionMultiplexerMock = new Mock <IConnectionMultiplexer>(MockBehavior.Strict); var dbMock = new Mock <IDatabase>(MockBehavior.Strict); var transactionMock = new Mock <ITransaction>(MockBehavior.Strict); transactionMock.Setup(redisTransaction => redisTransaction.SortedSetRemoveRangeByScoreAsync( cacheKey.ToString(), 0, utcDateTime.Ticks - (long)cacheKey.Unit, It.IsAny <Exclude>(), It.IsAny <CommandFlags>())).Returns(Task.FromResult(10L)); transactionMock.Setup(redisTransaction => redisTransaction.SortedSetAddAsync( cacheKey.ToString(), It.IsAny <RedisValue>(), utcDateTime.Ticks, It.IsAny <When>(), It.IsAny <CommandFlags>())).Returns(Task.FromResult(true)); transactionMock.Setup(redisTransaction => redisTransaction.SortedSetLengthAsync( cacheKey.ToString(), It.IsAny <double>(), It.IsAny <double>(), It.IsAny <Exclude>(), It.IsAny <CommandFlags>())).Returns( Task.FromResult(numberOfRequestsMadeSoFar)); transactionMock.Setup(redisTransaction => redisTransaction.KeyExpireAsync( cacheKey.ToString(), cacheKey.Expiration.Add(new TimeSpan(0, 1, 0)), It.IsAny <CommandFlags>())).Returns(Task.FromResult(true)); transactionMock.Setup(redisTransaction => redisTransaction.ExecuteAsync(CommandFlags.None)).Returns(Task.FromResult(true)); var postViolationTransactionMock = new Mock <ITransaction>(MockBehavior.Strict); postViolationTransactionMock.Setup(redisTransaction => redisTransaction.SortedSetRangeByRankWithScoresAsync( cacheKey.ToString(), 0, 0, It.IsAny <Order>(), It.IsAny <CommandFlags>())) .Returns(Task.FromResult(new SortedSetEntry[] { new SortedSetEntry(It.IsAny <RedisValue>(), utcDateTime.Ticks) })); postViolationTransactionMock.Setup(redisTransaction => redisTransaction.ExecuteAsync(CommandFlags.None)).Returns(Task.FromResult(true)); dbMock.SetupSequence(db => db.CreateTransaction(null)) .Returns(transactionMock.Object) .Returns(postViolationTransactionMock.Object); connectionMultiplexerMock.Setup(connection => connection.IsConnected).Returns(true); connectionMultiplexerMock.Setup(connection => connection.GetDatabase(-1, null)).Returns(dbMock.Object); var rateLimiter = new SlidingTimeWindowRateLimiter("http://localhost", clock: clockMock.Object, connectToRedisFunc: async() => await Task.FromResult(connectionMultiplexerMock.Object), countThrottledRequests: true); return(rateLimiter, connectionMultiplexerMock, dbMock, transactionMock, postViolationTransactionMock, clockMock); }