bucket_hydra v0.1.0 BucketHydra behaviour
A bucket-based rate limit algorithm with support for clusters.
Usage
Create a module that acts as the rate limiter:
defmodule MyApiRateLimit do
use BucketHydra,
pubsub: MyAppWeb.Endpoint,
size: 50,
time_scale: :timer.minutes(5)
end
Start your rate limiter in each node of the cluster:
children = [
# ...
MyApiRateLimit,
# ...
]
Consume space in a bucket with consume/2
, for instance in a plug that guards
your API:
def call(conn, opts) do
# ...
if MyApiRateLimit.consume(conn.assigns.api_key) do
conn
else
conn
|> send_resp(429, "Rate limit exceeded")
|> halt()
end
end
Clustering
BucketHydra was specifically built to act as a rate limiter in a multi-
node cluster. It syncronizes the usages across all instances of itself using
Phoenix.PubSub
broadcasts.
If you're not using phoenix, you can still use the phoenix_pubsub
library
and simply start a pubsub (a good starting point is Phoenix.PubSub.PG2
)
instance in your application supervisor.
Caveats
When a bucket only has one space left and 2 requests for the same bucket arrive at the same time, both might be allowed to happen.
In cases of a net split, the resulting clusters will work on their own and might allow more incoming requests in total. After the net split heals, all nodes are synchronized again.
Link to this section Summary
Types
:size
a positive integer of how big the bucket is, default 120
The key for a bucket can be anything but we recommend using primitive IDs such as UUIDs, integers or atoms.
Functions
Generated record functions to interact with buckets returned by get_bucket/2
.
Callbacks
Checks if the bucket for the given key still has enough space without consuming that space.
Checks if the bucket for the given key still has enough space, and then consumes that space.
Consumes space in the bucket without checking for capacity
Gets the bucket for the given key, time_scale and time (via bucket_opts).
Calculates the time bucket for the given scale that a request for the given time would fall into.
Link to this section Types
bucket_opt()
bucket_opt() :: {:size, non_neg_integer()} | {:time_scale, non_neg_integer()} | {:cost, non_neg_integer()}
:size
a positive integer of how big the bucket is, default 120
:time_scale
a positive integer in milliseconds how long a bucket lasts until it is emptied, default 60_000 ms
:cost
if consuming from a bucket should cost more than 1 use, specify the increased cost here, default 1
:for_time
default System.system_time(:millisecond)
- a different time (in milliseconds) can be passed so a
request might fall into an older or newer bucket.
consumption()
consumption() :: {:consumption, key :: key(), cost :: pos_integer()}
The key for a bucket can be anything but we recommend using primitive IDs such as UUIDs, integers or atoms.
key_record()
key_record() :: {:key, key :: key(), time_scale :: pos_integer(), time_bucket :: pos_integer()}
Link to this section Functions
Generated record functions to interact with buckets returned by get_bucket/2
.
Link to this section Callbacks
can_consume?(key, bucket_opts)
can_consume?(key(), bucket_opts()) :: boolean()
Checks if the bucket for the given key still has enough space without consuming that space.
Checks if the bucket for the given key still has enough space, and then consumes that space.
Returns true if there was space left and consumed.
Consumes space in the bucket without checking for capacity
get_bucket(key, bucket_opts)
get_bucket(key(), bucket_opts()) :: {:ok, bucket()}
Gets the bucket for the given key, time_scale and time (via bucket_opts).
time_bucket(time_scale, for_time)
time_bucket(time_scale :: pos_integer(), for_time :: pos_integer()) :: pos_integer()
Calculates the time bucket for the given scale that a request for the given time would fall into.