Current section
Files
Jump to
Current section
Files
src/speedbump.erl
-module(speedbump).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-define(FILEPATH, "src/speedbump.gleam").
-export([start/1, supervised/1, lazy_guard/4, ask/3, hits_per_second/1, hits_per_seconds/2, hits_per_minute/1, hits_per_minutes/2, hits_per_hour/1, hits_per_hours/2]).
-export_type([limit/0, msg/0, state/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type limit() :: {limit, integer(), integer(), integer(), binary()}.
-opaque msg() :: {hit,
gleam@erlang@process:subject({ok, nil} | {error, binary()})} |
{ask, gleam@erlang@process:subject(integer()), integer()}.
-opaque state() :: {state, list(limit()), integer()}.
-file("src/speedbump.gleam", 32).
?DOC(" Check whether a limit has been properly configured.\n").
-spec is_valid(limit()) -> boolean().
is_valid(Limit) ->
(((erlang:element(3, Limit) > 0) andalso (erlang:element(4, Limit) >= erlang:element(
3,
Limit
)))
andalso (erlang:element(2, Limit) > 0))
andalso (erlang:element(5, Limit) /= <<""/utf8>>).
-file("src/speedbump.gleam", 40).
-spec replenish_tokens(limit(), integer(), integer()) -> limit().
replenish_tokens(Limit, Last_hit_ns, Curr_time_ns) ->
Tokens = begin
_pipe = Curr_time_ns,
_pipe@1 = gleam@int:subtract(_pipe, Last_hit_ns),
_pipe@2 = gleam@int:divide(_pipe@1, erlang:element(2, Limit)),
_pipe@3 = gleam@result:unwrap(_pipe@2, 0),
_pipe@4 = gleam@int:add(_pipe@3, erlang:element(3, Limit)),
gleam@int:min(_pipe@4, erlang:element(4, Limit))
end,
_record = Limit,
{limit,
erlang:element(2, _record),
Tokens,
erlang:element(4, _record),
erlang:element(5, _record)}.
-file("src/speedbump.gleam", 65).
-spec hits_per_ns(integer(), integer(), binary()) -> limit().
hits_per_ns(Hits, Ns, Description) ->
Ns_per_token = case case Hits of
0 -> 0;
Gleam@denominator -> Ns rem Gleam@denominator
end of
0 ->
case Hits of
0 -> 0;
Gleam@denominator@1 -> Ns div Gleam@denominator@1
end;
_ ->
(case Hits of
0 -> 0;
Gleam@denominator@2 -> Ns div Gleam@denominator@2
end) + 1
end,
{limit, Ns_per_token, Hits, Hits, Description}.
-file("src/speedbump.gleam", 177).
-spec handle_msg(state(), msg()) -> gleam@otp@actor:next(state(), msg()).
handle_msg(State, Msg) ->
Curr_time_ns = ffi:nanosecond(),
Updated_limits@1 = gleam@list:fold(
erlang:element(2, State),
[],
fun(Updated_limits, Limit) ->
_pipe = replenish_tokens(
Limit,
erlang:element(3, State),
Curr_time_ns
),
gleam@list:prepend(Updated_limits, _pipe)
end
),
State@1 = begin
_record = State,
{state, Updated_limits@1, erlang:element(3, _record)}
end,
case Msg of
{hit, Reply_with} ->
Res = gleam@list:try_fold(
erlang:element(2, State@1),
[],
fun(Updated_limits@2, Limit@1) ->
case erlang:element(3, Limit@1) of
0 ->
{error, Limit@1};
X ->
{ok,
[begin
_record@1 = Limit@1,
{limit,
erlang:element(2, _record@1),
X - 1,
erlang:element(4, _record@1),
erlang:element(5, _record@1)}
end |
Updated_limits@2]}
end
end
),
case Res of
{error, Limit@2} ->
gleam@erlang@process:send(
Reply_with,
{error, erlang:element(5, Limit@2)}
),
gleam@otp@actor:continue(State@1);
{ok, Updated_limits@3} ->
gleam@erlang@process:send(Reply_with, {ok, nil}),
gleam@otp@actor:continue(
begin
_record@2 = State@1,
{state,
Updated_limits@3,
erlang:element(3, _record@2)}
end
)
end;
{ask, Reply_with@1, N_requests} ->
Wait = gleam@list:fold(
erlang:element(2, State@1),
0,
fun(Wait_remaining, Limit@3) ->
Cost_of_limit = begin
Not_free_requests = N_requests - erlang:element(
3,
Limit@3
),
Partial_waiting_period_ns = ffi:nanosecond() - erlang:element(
3,
State@1
),
case Not_free_requests of
0 ->
0;
1 ->
Partial_waiting_period_ns;
_ ->
Partial_waiting_period_ns + (erlang:element(
2,
Limit@3
)
* (Not_free_requests - 1))
end
end,
gleam@int:max(Wait_remaining, Cost_of_limit)
end
),
gleam@erlang@process:send(Reply_with@1, Wait),
gleam@otp@actor:continue(State@1)
end.
-file("src/speedbump.gleam", 257).
?DOC(" Start a new rate limiter actor.\n").
-spec start(list(limit())) -> {ok,
gleam@otp@actor:started(gleam@erlang@process:subject(msg()))} |
{error, gleam@otp@actor:start_error()}.
start(Limits) ->
_pipe@5 = gleam@otp@actor:new_with_initialiser(
1000,
fun(Subj) ->
gleam@bool:guard(
gleam@list:any(Limits, fun(Limit) -> not is_valid(Limit) end),
{error, <<"invalid limit"/utf8>>},
fun() -> _pipe = {state, Limits, ffi:nanosecond()},
_pipe@1 = gleam@otp@actor:initialised(_pipe),
_pipe@3 = gleam@otp@actor:selecting(
_pipe@1,
begin
_pipe@2 = gleam_erlang_ffi:new_selector(),
gleam@erlang@process:select(_pipe@2, Subj)
end
),
_pipe@4 = gleam@otp@actor:returning(_pipe@3, Subj),
{ok, _pipe@4} end
)
end
),
_pipe@6 = gleam@otp@actor:on_message(_pipe@5, fun handle_msg/2),
gleam@otp@actor:start(_pipe@6).
-file("src/speedbump.gleam", 278).
?DOC(" Get a constructor for a rate limiter that can be used as part of a larger supervision tree.\n").
-spec supervised(list(limit())) -> gleam@otp@supervision:child_specification(gleam@erlang@process:subject(msg())).
supervised(Limits) ->
gleam@otp@supervision:worker(fun() -> start(Limits) end).
-file("src/speedbump.gleam", 292).
?DOC(
" This function allows the rate limiter to be used as a lazy guard.\n"
" Example:\n"
" ```gleam\n"
" use <- speedbump.lazy_guard(limiter, fn(limit_description) {\n"
" // ... Construct the appropriate error. `limit_description` is a text description of the limit that was violated.\n"
" })\n"
" // ... Continue with the function\n"
" ```\n"
).
-spec lazy_guard(
gleam@otp@actor:started(gleam@erlang@process:subject(msg())),
integer(),
fun((binary()) -> FSE),
fun(() -> FSE)
) -> FSE.
lazy_guard(Rate_limiter, Timeout_ms, Or_else, Do) ->
case gleam@erlang@process:call(
erlang:element(3, Rate_limiter),
Timeout_ms,
fun(Field@0) -> {hit, Field@0} end
) of
{error, Desc} ->
Or_else(Desc);
{ok, nil} ->
Do()
end.
-file("src/speedbump.gleam", 309).
?DOC(
" Ask the rate limiter how much time is left before you can make n requests.\n"
" The response is in *nanoseconds*.\n"
).
-spec ask(
gleam@otp@actor:started(gleam@erlang@process:subject(msg())),
integer(),
integer()
) -> integer().
ask(Rate_limiter, Timeout_ms, N_requests) ->
gleam@erlang@process:call(
erlang:element(3, Rate_limiter),
Timeout_ms,
fun(_capture) -> {ask, _capture, N_requests} end
).
-file("src/speedbump.gleam", 91).
?DOC(" Creates a limit of `hits` number of requests per second.\n").
-spec hits_per_second(integer()) -> limit().
hits_per_second(Hits) ->
hits_per_ns(
Hits,
1000000000,
<<(erlang:integer_to_binary(Hits))/binary, " requests per second"/utf8>>
).
-file("src/speedbump.gleam", 100).
?DOC(" Creates a limit of `hits` number of requests per `seconds` seconds.\n").
-spec hits_per_seconds(integer(), integer()) -> limit().
hits_per_seconds(Hits, Secs) ->
hits_per_ns(
Hits,
1000000000 * Secs,
<<<<<<(erlang:integer_to_binary(Hits))/binary, " requests per "/utf8>>/binary,
(erlang:integer_to_binary(Secs))/binary>>/binary,
" seconds"/utf8>>
).
-file("src/speedbump.gleam", 112).
?DOC(" Creates a limit of `hits` number of requests per minute.\n").
-spec hits_per_minute(integer()) -> limit().
hits_per_minute(Hits) ->
hits_per_ns(
Hits,
1000000000 * 60,
<<(erlang:integer_to_binary(Hits))/binary, " requests per minute"/utf8>>
).
-file("src/speedbump.gleam", 121).
?DOC(" Creates a limit of `hits` number of requests per `minutes` minutes.\n").
-spec hits_per_minutes(integer(), integer()) -> limit().
hits_per_minutes(Hits, Mins) ->
hits_per_ns(
Hits,
(1000000000 * 60) * Mins,
<<<<<<(erlang:integer_to_binary(Hits))/binary, " requests per "/utf8>>/binary,
(erlang:integer_to_binary(Mins))/binary>>/binary,
" minutes"/utf8>>
).
-file("src/speedbump.gleam", 133).
?DOC(" Creates a limit of `hits` number of requests per hour.\n").
-spec hits_per_hour(integer()) -> limit().
hits_per_hour(Hits) ->
hits_per_ns(
Hits,
(1000000000 * 60) * 60,
<<(erlang:integer_to_binary(Hits))/binary, " requests per hour"/utf8>>
).
-file("src/speedbump.gleam", 142).
?DOC(" Creates a limit of `hits` number of requests per `hours` hours.\n").
-spec hits_per_hours(integer(), integer()) -> limit().
hits_per_hours(Hits, Hrs) ->
hits_per_ns(
Hits,
((1000000000 * 60) * 60) * Hrs,
<<<<<<(erlang:integer_to_binary(Hits))/binary, " requests per "/utf8>>/binary,
(erlang:integer_to_binary(Hrs))/binary>>/binary,
" hours"/utf8>>
).