-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 ).