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); }
/// <summary> /// Rate limits a request using to cache key provided /// </summary> /// <param name="cacheKey">The cache key.</param> /// <returns></returns> protected abstract Func <long> GetNumberOfRequestsAsync( string requestId, string method, string host, string routeTemplate, AllowedConsumptionRate policy, IList <RateLimitCacheKey> cacheKeys, ITransaction redisTransaction, long utcNowTicks, int costPerCall = 1);
protected static void GetDateRange(AllowedConsumptionRate allowedCallRate, DateTime dateTimeUtc, out DateTime fromUtc, out DateTime toUtc) { var periodUnits = allowedCallRate.Period.Repeating ? Math.Floor(dateTimeUtc.Subtract(allowedCallRate.Period.StartDateTimeUtc).TotalHours / allowedCallRate.Period.Duration.TotalHours) : 0; fromUtc = periodUnits > 0 ? allowedCallRate.Period.StartDateTimeUtc.Add( new TimeSpan(allowedCallRate.Period.Duration.Ticks * Convert.ToInt64(periodUnits))) : allowedCallRate.Period.StartDateTimeUtc; toUtc = fromUtc.Add(allowedCallRate.Period.Duration); }
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 long GetTicksPerUnit(AllowedConsumptionRate allowedCallRate) { return(allowedCallRate.Unit != RateLimitUnit.PerCustomPeriod ? (long)allowedCallRate.Unit : allowedCallRate.Period.Duration.Ticks); }
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); }