/// <summary>
        /// Adds a new client request to the manager, if it isn't exceeding
        /// the capacity.
        /// </summary>
        /// <param name="clientId">
        /// The identifier of the client which initiated the request.
        /// </param>
        /// <param name="requestId">
        /// The request identifier.
        /// </param>
        /// <param name="timestamp">
        /// The timestamp of the request.
        /// </param>
        /// <returns>
        /// The <see cref="AddConcurrentRequestResult"/> instance containing the response.
        /// </returns>
        public async Task <AddConcurrentRequestResult> AddAsync(string clientId, string requestId, long timestamp)
        {
            var ttl      = _configuration.RequestTimeToLive;
            var capacity = _configuration.Capacity;
            var key      = $"{_configuration.KeysPrefix}.{clientId}";

            if (_preparedLuaScript == null)
            {
                var rawLuaScript = await ResourceLoader.GetResourceAsync(ScriptName);

                _preparedLuaScript = LuaScript.Prepare(rawLuaScript);
            }

            try
            {
                _logger.LogDebug("Received request {0} from client {1} with timestamp {2}.",
                                 requestId, clientId, timestamp);

                var database = _redisClient.GetDatabase();

                await database.SortedSetRemoveRangeByScoreAsync(key, double.NegativeInfinity, timestamp - ttl);

                var parameters = new
                {
                    capacity  = capacity,
                    timestamp = timestamp,
                    requestId = requestId,
                    key       = (RedisKey)key
                };

                var result = (RedisResult[])await _preparedLuaScript.EvaluateAsync(database, parameters);

                var isAllowed = (bool)result[0];
                var remaining = _configuration.Capacity - (int)result[1];

                _logger.LogInformation(
                    isAllowed
                        ? "Allowed request {0} from client {1} with timestamp {2}. The client has {3} request(s) remaining."
                        : "Rejected request {0} from client {1} with timestamp {2}. The client has {3} request(s) remaining.",
                    requestId, clientId, timestamp, remaining);

                return(new AddConcurrentRequestResult(
                           isAllowed: isAllowed,
                           remaining: remaining,
                           limit: capacity
                           ));
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Unhandled Redis exception: {0}", ex.Message);

                // Fail open so Redis outage doesn't take down everything
                // with it.
                return(new AddConcurrentRequestResult(
                           isAllowed: true,
                           remaining: capacity - 1,
                           limit: capacity
                           ));
            }
        }
Example #2
0
        /// <summary>
        /// Consumes requested number of tokens for the specified client.
        /// </summary>
        /// <param name="clientId">
        /// The client identifier.
        /// </param>
        /// <param name="requested">
        /// The number of tokens to consume.
        /// </param>
        /// <returns>
        /// The consumption result.
        /// </returns>
        public async Task <ConsumeResult> ConsumeAsync(string clientId, int requested)
        {
            if (_preparedLuaScript == null)
            {
                var rawLuaScript = await ResourceLoader.GetResourceAsync(ScriptName);

                _preparedLuaScript = LuaScript.Prepare(rawLuaScript);
            }

            var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            var capacity  = _configuration.AverageRate * _configuration.Bursting;

            var parameters = new
            {
                refill_rate   = _configuration.AverageRate,
                refill_time   = _configuration.Interval,
                capacity      = capacity,
                timestamp     = timestamp,
                requested     = 1,
                tokens_key    = (RedisKey)$"{_configuration.KeysPrefix}.{clientId}.tokens",
                timestamp_key = (RedisKey)$"{_configuration.KeysPrefix}.{clientId}.timestamp"
            };

            try
            {
                _logger.LogDebug("Received request from client {0} with timestamp {1}.", clientId, timestamp);

                var database = _redisClient.GetDatabase();

                // TODO: Send hash instead of full script?
                var result = (RedisResult[])await _preparedLuaScript.EvaluateAsync(database, parameters);

                var response = new ConsumeResult
                               (
                    isAllowed: !result[0].IsNull && (bool)result[0],
                    remaining: !result[1].IsNull ? (int)result[1] : 0,
                    limit: capacity
                               );

                _logger.LogInformation(
                    response.IsAllowed
                        ? "Allowed request from client {0} with timestamp {1}. The client has {2} token(s) remaining."
                        : "Rejected request from client {0} with timestamp {1}. The client has {2} token(s) remaining.",
                    clientId, timestamp, response.Remaining);

                return(response);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Unhandled Redis exception: {0}", ex.Message);

                // Fail open if there is an exception.
                return(new ConsumeResult(
                           isAllowed: true,
                           remaining: capacity - 1,
                           limit: capacity
                           ));
            }
        }
 public Task <RedisResult> ScriptEvaluateAsync(LuaScript script, object parameters = null, CommandFlags flags = CommandFlags.None)
 {
     // TODO: The return value could contain prefixed keys. It might make sense to 'unprefix' those?
     return(script.EvaluateAsync(Inner, parameters, Prefix, flags));
 }