/// <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 )); } }
/// <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)); }