livery_ratelimit_store (livery v0.2.0)

View Source

Owner and store for livery_ratelimit token buckets.

A supervised gen_server that owns one public named ETS table and reaps idle buckets on a timer. The per-request token-bucket decision (check/5) runs in the calling process directly against the public table (lock-free CAS), so the gen_server is never on the hot path - it only owns the table and runs cleanup. The table is public so requests update buckets without serializing through the owner; this is safe because Livery runs no untrusted in-VM code, and making it protected would force every check through the owner and reintroduce exactly the single-process bottleneck the lock-free design avoids.

Each row is {{Name, KeyDigest}, Tokens, LastMicros, Cap, Rate}. KeyDigest is a SHA-256 of the rate-limit key, so raw bearer tokens are never stored. Cap/Rate are denormalized so the sweep can compute refill without the limiter config.

The table is bounded: once it holds ratelimit_max_keys rows (application environment, default 1,000,000) new keys are shed ({deny, undefined}) rather than inserted, so a flood of distinct keys cannot grow the table without limit. Idle buckets are reaped every minute, so the bound is also released as load drops.

Summary

Functions

Token-bucket decision for {Name, KeyDigest}.

Reap fully-refilled buckets now; returns the number removed.

Types

result()

-type result() ::
          {allow, float(), non_neg_integer() | undefined} | {deny, non_neg_integer() | undefined}.

state()

-type state() :: #state{}.

Functions

check(Name, KeyDigest, Cap, Rate, Now)

-spec check(term(), binary(), non_neg_integer(), number(), integer()) -> result().

Token-bucket decision for {Name, KeyDigest}.

Returns {allow, RemainingTokens, ResetSecs} (a token was consumed) or {deny, RetryAfterSecs}. ResetSecs/RetryAfterSecs are undefined when Rate =< 0 (no refill). Runs in the caller; lock-free.

handle_call/3

-spec handle_call(term(), gen_server:from(), state()) -> {reply, term(), state()}.

handle_cast(Request, State)

-spec handle_cast(term(), state()) -> {noreply, state()}.

handle_info/2

-spec handle_info(term(), state()) -> {noreply, state()}.

init/1

-spec init([]) -> {ok, state()}.

start_link()

-spec start_link() -> {ok, pid()} | {error, term()}.

sweep()

-spec sweep() -> non_neg_integer().

Reap fully-refilled buckets now; returns the number removed.