Packages

Gleam + Redis token bucket rate limiting library

Current section

Files

Jump to
crabbucket_redis src crabbucket@redis.erl
Raw

src/crabbucket@redis.erl

-module(crabbucket@redis).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-export([clear_key/2, remaining_tokens_for_key/3]).
-export_type([redis_token_bucket/0, remaining_token_count_success/0, remaining_token_count_failure/0]).
-type redis_token_bucket() :: {redis_token_bucket,
gleam@erlang@process:subject(radish@client:message()),
integer()}.
-type remaining_token_count_success() :: {has_remaining_tokens,
integer(),
integer()}.
-type remaining_token_count_failure() :: {must_wait_until, integer()} |
{redis_error, radish@error:error()}.
-spec clear_key(redis_token_bucket(), binary()) -> {ok, boolean()} |
{error, radish@error:error()}.
clear_key(Bucket, Key) ->
gleam@result:'try'(
(radish@hash:del(
erlang:element(2, Bucket),
Key,
[<<"window_start"/utf8>>, <<"tokens"/utf8>>],
500
)),
fun(Result) -> {ok, Result > 0} end
).
-spec remaining_tokens_for_key(redis_token_bucket(), binary(), integer()) -> {ok,
remaining_token_count_success()} |
{error, remaining_token_count_failure()}.
remaining_tokens_for_key(Bucket, Key, Default_token_count) ->
Cmd = radish@utils:prepare(
[<<"EVAL"/utf8>>,
<<"local key = KEYS[1]
local window_start_arg = tonumber(ARGV[1])
local window_duration_arg = tonumber(ARGV[2])
local tokens_arg = tonumber(ARGV[3])
local expiration_seconds = math.floor(window_duration_arg / 1000) + 1
local window_start = tonumber(redis.call('HGET', key, 'window_start'))
local tokens = tonumber(redis.call('HGET', key, 'tokens'))
if window_start == nil or tokens == nil then
redis.call('HSET', key, 'window_start', window_start_arg, 'tokens', tokens_arg)
redis.call('EXPIRE', key, expiration_seconds)
return {tokens_arg, window_start_arg + window_duration_arg}
end
local time_arr = redis.call('TIME')
local time_ms = (time_arr[1] * 1000) + (math.floor(time_arr[2] / 1000))
if (window_start + window_duration_arg) < time_ms then
redis.call('HSET', key, 'window_start', window_start_arg, 'tokens', tokens_arg)
redis.call('EXPIRE', key, expiration_seconds)
return {tokens_arg, window_start_arg + window_duration_arg}
end
if tokens <= 0 then
return {-1, window_start + window_duration_arg}
end
redis.call('HSET', key, 'tokens', tokens - 1)
return {tokens - 1, window_start + window_duration_arg}"/utf8>>,
<<"1"/utf8>>,
Key,
begin
_pipe = os:system_time(1000),
gleam@int:to_string(_pipe)
end,
begin
_pipe@1 = erlang:element(3, Bucket),
gleam@int:to_string(_pipe@1)
end,
begin
_pipe@2 = Default_token_count - 1,
gleam@int:to_string(_pipe@2)
end]
),
gleam@result:'try'(
begin
_pipe@3 = radish@utils:execute(erlang:element(2, Bucket), Cmd, 500),
gleam@result:map_error(_pipe@3, fun(E) -> {redis_error, E} end)
end,
fun(Results) -> case Results of
[{array, [{integer, Tokens_remaining}, {integer, Next_reset}]}] ->
case Tokens_remaining < 0 of
true ->
{error, {must_wait_until, Next_reset}};
false ->
{ok,
{has_remaining_tokens,
Tokens_remaining,
Next_reset}}
end;
Other ->
gleam@io:debug(Other),
erlang:error(#{gleam_error => panic,
message => <<"Should be unreachable"/utf8>>,
module => <<"crabbucket/redis"/utf8>>,
function => <<"remaining_tokens_for_key"/utf8>>,
line => 123})
end end
).