Counts events per key over sliding windows, for limits such as "100 messages per hour per client address".
children = [
{Sovite.Abuse.RateLimit, name: MyApp.RateLimit}
]
case RateLimit.hit(MyApp.RateLimit, ip, 100, 3_600_000) do
:ok -> accept()
:limited -> defer()
endEach window is split into fixed buckets of its own length, and the count is estimated from the current bucket plus the share of the previous one that still falls inside the window. This needs two counters per key and window, and is accurate enough for abuse limits. One table can hold limits with different windows for the same key.
Callers update an ETS table directly and never wait on the process, which only owns the table and purges old buckets. Concurrent hits can refuse a little more than strictly needed, never less. State is in memory: a restart forgets all counts.
Options
:name- an atom, also used as the ETS table name. Required.:cleanup_interval- milliseconds between purges of old buckets. Defaults to one minute.:clock- a zero-arity function returning the current time in milliseconds. Defaults to the time since the VM started.:replicate- whentrue, counted events are also kept as deltas, fortake_deltas/1. Defaults tofalse.
Sharing counts
Several nodes can enforce one limit together: each takes the deltas
of its table (take_deltas/1) now and then, and sends them to the
others, which add them to theirs (merge/2). The nodes need the same
:clock, such as &System.os_time(:millisecond)/0 on hosts with
synchronized clocks, so that their buckets line up. Events counted
elsewhere and not yet merged are missing from the counts, so limits
are a little looser than on one node.
Telemetry
[:sovite, :abuse, :rate_limit, :exceeded]-%{limit, window},%{rate_limit, key}, on the first refused hit of a key and window in each bucket.
Summary
Functions
Counts count events for key without a limit. Returns the number of
events in the last window milliseconds, rounded down.
Returns the number of events for key in the last window milliseconds, rounded down.
Counts count events for key unless that would take it over limit
events in the last window milliseconds. A refused hit is not counted.
Adds events counted on other nodes (see take_deltas/1).
Forgets every window of key.
Starts the rate limiter.
Returns the events counted since the last call, as {key, window, bucket, count} tuples, for merge/2 on other nodes. Empty unless
started with replicate: true.
Functions
@spec add(atom(), term(), pos_integer(), pos_integer()) :: non_neg_integer()
Counts count events for key without a limit. Returns the number of
events in the last window milliseconds, rounded down.
@spec count(atom(), term(), pos_integer()) :: non_neg_integer()
Returns the number of events for key in the last window milliseconds, rounded down.
@spec hit(atom(), term(), non_neg_integer(), pos_integer(), pos_integer()) :: :ok | :limited
Counts count events for key unless that would take it over limit
events in the last window milliseconds. A refused hit is not counted.
@spec merge(atom(), [{term(), pos_integer(), integer(), pos_integer()}]) :: :ok
Adds events counted on other nodes (see take_deltas/1).
Forgets every window of key.
@spec start_link(keyword()) :: GenServer.on_start()
Starts the rate limiter.
@spec take_deltas(atom()) :: [{term(), pos_integer(), integer(), pos_integer()}]
Returns the events counted since the last call, as {key, window, bucket, count} tuples, for merge/2 on other nodes. Empty unless
started with replicate: true.