Current section

Files

Jump to
glimit src glimit.erl
Raw

src/glimit.erl

-module(glimit).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glimit.gleam").
-export([new/0, per_second/2, per_second_fn/2, burst_limit/2, burst_limit_fn/2, on_limit_exceeded/2, identifier/2, build/1, apply_built/2, apply/2]).
-export_type([rate_limiter/3, rate_limiter_builder/3]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" This module provides a distributed rate limiter that can be used to limit the\n"
" number of requests or function calls per second for a given identifier.\n"
"\n"
" A single actor is used to assign one rate limiter actor per identifier. The\n"
" rate limiter actor then uses a Token Bucket algorithm to determine if a\n"
" request or function call should be allowed to proceed. A separate process is\n"
" polling the rate limiters to remove full buckets to reduce unnecessary memory\n"
" usage.\n"
"\n"
" The rate limits are configured using the following two options:\n"
"\n"
" - `per_second`: The rate of new available tokens per second. Think of this\n"
" as the steady state rate limit.\n"
" - `burst_limit`: The maximum number of available tokens. Think of this as\n"
" the burst rate limit. The default value is the `per_second` rate limit.\n"
"\n"
" The rate limiter can be applied to a function or handler using the `apply`\n"
" function, which returns a new function that checks the rate limit before\n"
" calling the original function.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" import glimit\n"
"\n"
" let limiter =\n"
" glimit.new()\n"
" |> glimit.per_second(10)\n"
" |> glimit.burst_limit(100)\n"
" |> glimit.identifier(fn(request) { request.ip })\n"
" |> glimit.on_limit_exceeded(fn(_request) { \"Rate limit reached\" })\n"
"\n"
" let handler =\n"
" fn(_request) { \"Hello, world!\" }\n"
" |> glimit.apply(limiter)\n"
" ```\n"
"\n"
).
-type rate_limiter(FVC, FVD, FVE) :: {rate_limiter,
gleam@erlang@process:subject(glimit@registry:message(FVE)),
fun((FVC) -> FVD),
fun((FVC) -> FVE)}.
-type rate_limiter_builder(FVF, FVG, FVH) :: {rate_limiter_builder,
gleam@option:option(fun((FVH) -> integer())),
gleam@option:option(fun((FVH) -> integer())),
gleam@option:option(fun((FVF) -> FVH)),
gleam@option:option(fun((FVF) -> FVG))}.
-file("src/glimit.gleam", 67).
?DOC(" Create a new rate limiter builder.\n").
-spec new() -> rate_limiter_builder(any(), any(), any()).
new() ->
{rate_limiter_builder, none, none, none, none}.
-file("src/glimit.gleam", 95).
?DOC(
" Set the rate of new available tokens per second.\n"
"\n"
" Note that this is not the maximum number of requests that can be made in a single\n"
" second, but the rate at which tokens are added to the bucket. Think of this as the\n"
" steady state rate limit, while the `burst_limit` function sets the maximum number of\n"
" available tokens (or the burst rate limit).\n"
"\n"
" This value is also used as the default value for the `burst_limit` function.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" import glimit\n"
"\n"
" let limiter =\n"
" glimit.new()\n"
" |> glimit.per_second(10)\n"
" ```\n"
).
-spec per_second(rate_limiter_builder(FVO, FVP, FVQ), integer()) -> rate_limiter_builder(FVO, FVP, FVQ).
per_second(Limiter, Limit) ->
{rate_limiter_builder,
{some, fun(_) -> Limit end},
erlang:element(3, Limiter),
erlang:element(4, Limiter),
erlang:element(5, Limiter)}.
-file("src/glimit.gleam", 117).
?DOC(
" Set the rate limit per second, based on the identifier.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" import glimit\n"
"\n"
" let limiter =\n"
" glimit.new()\n"
" |> glimit.identifier(fn(request) { request.user_id })\n"
" |> glimit.per_second_fn(fn(user_id) {\n"
" db.get_rate_limit(user_id)\n"
" })\n"
" ```\n"
).
-spec per_second_fn(
rate_limiter_builder(FVX, FVY, FVZ),
fun((FVZ) -> integer())
) -> rate_limiter_builder(FVX, FVY, FVZ).
per_second_fn(Limiter, Limit_fn) ->
{rate_limiter_builder,
{some, Limit_fn},
erlang:element(3, Limiter),
erlang:element(4, Limiter),
erlang:element(5, Limiter)}.
-file("src/glimit.gleam", 140).
?DOC(
" Set the maximum number of available tokens.\n"
"\n"
" The maximum number of available tokens is the maximum number of requests that can be\n"
" made in a single second. The default value is the same as the rate limit per second.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" import glimit\n"
"\n"
" let limiter =\n"
" glimit.new()\n"
" |> glimit.per_second(10)\n"
" |> glimit.burst_limit(100)\n"
" ```\n"
).
-spec burst_limit(rate_limiter_builder(FWG, FWH, FWI), integer()) -> rate_limiter_builder(FWG, FWH, FWI).
burst_limit(Limiter, Burst_limit) ->
{rate_limiter_builder,
erlang:element(2, Limiter),
{some, fun(_) -> Burst_limit end},
erlang:element(4, Limiter),
erlang:element(5, Limiter)}.
-file("src/glimit.gleam", 163).
?DOC(
" Set the maximum number of available tokens, based on the identifier.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" import glimit\n"
"\n"
" let limiter =\n"
" glimit.new()\n"
" |> glimit.identifier(fn(request) { request.user_id })\n"
" |> glimit.per_second(10)\n"
" |> glimit.burst_limit_fn(fn(user_id) {\n"
" db.get_burst_limit(user_id)\n"
" })\n"
" ```\n"
).
-spec burst_limit_fn(
rate_limiter_builder(FWP, FWQ, FWR),
fun((FWR) -> integer())
) -> rate_limiter_builder(FWP, FWQ, FWR).
burst_limit_fn(Limiter, Burst_limit_fn) ->
{rate_limiter_builder,
erlang:element(2, Limiter),
{some, Burst_limit_fn},
erlang:element(4, Limiter),
erlang:element(5, Limiter)}.
-file("src/glimit.gleam", 183).
?DOC(
" Set the handler to be called when the rate limit is reached.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" import glimit\n"
"\n"
" let limiter =\n"
" glimit.new()\n"
" |> glimit.per_second(10)\n"
" |> glimit.on_limit_exceeded(fn(_request) { \"Rate limit reached\" })\n"
" ```\n"
).
-spec on_limit_exceeded(rate_limiter_builder(FWY, FWZ, FXA), fun((FWY) -> FWZ)) -> rate_limiter_builder(FWY, FWZ, FXA).
on_limit_exceeded(Limiter, On_limit_exceeded) ->
{rate_limiter_builder,
erlang:element(2, Limiter),
erlang:element(3, Limiter),
erlang:element(4, Limiter),
{some, On_limit_exceeded}}.
-file("src/glimit.gleam", 202).
?DOC(
" Set the identifier function to be used to identify the rate limit.\n"
"\n"
" # Example\n"
"\n"
" ```gleam\n"
" import glimit\n"
"\n"
" let limiter =\n"
" glimit.new()\n"
" |> glimit.identifier(fn(request) { request.ip })\n"
" ```\n"
).
-spec identifier(rate_limiter_builder(FXH, FXI, FXJ), fun((FXH) -> FXJ)) -> rate_limiter_builder(FXH, FXI, FXJ).
identifier(Limiter, Identifier) ->
{rate_limiter_builder,
erlang:element(2, Limiter),
erlang:element(3, Limiter),
{some, Identifier},
erlang:element(5, Limiter)}.
-file("src/glimit.gleam", 218).
?DOC(
" Build the rate limiter.\n"
"\n"
" Note that using `apply` will already build the rate limiter, so this function is\n"
" only useful if you want to build the rate limiter manually and apply it to multiple\n"
" functions.\n"
"\n"
" To apply the resulting rate limiter to a function or handler, use the `apply_built`\n"
" function.\n"
).
-spec build(rate_limiter_builder(FXQ, FXR, FXS)) -> {ok,
rate_limiter(FXQ, FXR, FXS)} |
{error, binary()}.
build(Config) ->
gleam@result:'try'(case erlang:element(2, Config) of
{some, Per_second} ->
{ok, Per_second};
none ->
{error, <<"`per_second` rate limit is required"/utf8>>}
end, fun(Per_second@1) ->
Burst_limit@1 = case erlang:element(3, Config) of
{some, Burst_limit} ->
Burst_limit;
none ->
Per_second@1
end,
gleam@result:'try'(
begin
_pipe = glimit@registry:new(Per_second@1, Burst_limit@1),
gleam@result:map_error(
_pipe,
fun(_) ->
<<"Failed to start rate limiter registry"/utf8>>
end
)
end,
fun(Rate_limiter_registry) ->
gleam@result:'try'(case erlang:element(4, Config) of
{some, Identifier} ->
{ok, Identifier};
none ->
{error,
<<"`identifier` function is required"/utf8>>}
end, fun(Identifier@1) ->
gleam@result:'try'(case erlang:element(5, Config) of
{some, On_limit_exceeded} ->
{ok, On_limit_exceeded};
none ->
{error,
<<"`on_limit_exceeded` function is required"/utf8>>}
end, fun(On_limit_exceeded@1) ->
{ok,
{rate_limiter,
Rate_limiter_registry,
On_limit_exceeded@1,
Identifier@1}}
end)
end)
end
)
end).
-file("src/glimit.gleam", 270).
?DOC(
" Apply the rate limiter to a request handler or function.\n"
"\n"
" This function is useful if you want to build the rate limiter manually using the\n"
" `build` function.\n"
).
-spec apply_built(fun((FYH) -> FYI), rate_limiter(FYH, FYI, any())) -> fun((FYH) -> FYI).
apply_built(Func, Limiter) ->
fun(Input) ->
Identifier = (erlang:element(4, Limiter))(Input),
case begin
_pipe = erlang:element(2, Limiter),
glimit@registry:get_or_create(_pipe, Identifier)
end of
{ok, Rate_limiter} ->
case begin
_pipe@1 = Rate_limiter,
glimit@rate_limiter:hit(_pipe@1)
end of
{ok, nil} ->
Func(Input);
{error, nil} ->
(erlang:element(3, Limiter))(Input)
end;
{error, _} ->
erlang:error(#{gleam_error => panic,
message => <<"Failed to get rate limiter"/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"glimit"/utf8>>,
function => <<"apply_built"/utf8>>,
line => 283})
end
end.
-file("src/glimit.gleam", 254).
?DOC(
" Apply the rate limiter to a request handler or function.\n"
"\n"
" Panics if the rate limiter registry cannot be started or if the `identifier`\n"
" function or `on_limit_exceeded` function is missing.\n"
).
-spec apply(fun((FYB) -> FYC), rate_limiter_builder(FYB, FYC, any())) -> fun((FYB) -> FYC).
apply(Func, Config) ->
Limiter@1 = case build(Config) of
{ok, Limiter} ->
Limiter;
{error, Message} ->
erlang:error(#{gleam_error => panic,
message => Message,
file => <<?FILEPATH/utf8>>,
module => <<"glimit"/utf8>>,
function => <<"apply"/utf8>>,
line => 260})
end,
apply_built(Func, Limiter@1).