Packages

Resilience library for the BEAM - circuit breaking, rate limiting, bulkheads, and retry

Current section

Files

Jump to
seki src seki_backend_pg.erl
Raw

src/seki_backend_pg.erl

-module(seki_backend_pg).
-moduledoc """
Distributed rate limiter backend using Erlang's `pg` module.
Each node maintains local ETS counters. A gossip process (`seki_pg_gossip`)
periodically broadcasts state to peers, which merge using last-writer-wins.
Eventually consistent — suitable for most rate limiting use cases.
## Options
- `scope` — pg scope atom (default: `seki_pg`)
- `group` — pg group atom (default: `seki_limiters`)
- `gossip_interval` — broadcast interval in ms (default: 1000)
""".
-behaviour(seki_backend).
-export([
init/1,
get/2,
put/3,
update/4,
delete/2,
cleanup/2,
terminate/1
]).
-record(state, {
local_tab :: ets:tid(),
gossip_pid :: pid(),
scope :: atom()
}).
%%----------------------------------------------------------------------
%% Backend callbacks
%%----------------------------------------------------------------------
init(Opts) ->
Scope = maps:get(scope, Opts, seki_pg),
Group = maps:get(group, Opts, seki_limiters),
TableName = maps:get(table_name, Opts, seki_pg_data),
%% Ensure pg scope exists
ok = ensure_pg_scope(Scope),
%% Local ETS for fast reads/writes
Tab = ets:new(TableName, [
set,
public,
{read_concurrency, true},
{write_concurrency, true}
]),
%% Start gossip process
GossipInterval = maps:get(gossip_interval, Opts, 1000),
{ok, GossipPid} = seki_pg_gossip:start_link(#{
scope => Scope,
group => Group,
tab => Tab,
interval => GossipInterval
}),
logger:info(
"Distributed backend started (scope=~p, group=~p, gossip_interval=~pms)",
[Scope, Group, GossipInterval],
#{domain => [seki]}
),
{ok, #state{
local_tab = Tab,
gossip_pid = GossipPid,
scope = Scope
}}.
get(#state{local_tab = Tab}, Key) ->
case ets:lookup(Tab, Key) of
[{Key, Value}] -> {ok, Value};
[] -> not_found
end.
put(#state{local_tab = Tab}, Key, Value) ->
true = ets:insert(Tab, {Key, Value}),
ok.
update(#state{local_tab = Tab}, Key, Fun, Default) ->
NewValue =
case ets:lookup(Tab, Key) of
[{Key, Value}] -> Fun(Value);
[] -> Fun(Default)
end,
true = ets:insert(Tab, {Key, NewValue}),
{ok, NewValue}.
delete(#state{local_tab = Tab}, Key) ->
true = ets:delete(Tab, Key),
ok.
cleanup(#state{local_tab = Tab}, OlderThan) ->
Now = erlang:monotonic_time(millisecond),
Cutoff = Now - OlderThan,
ets:foldl(
fun
({Key, {_, _, Ts}}, Acc) when Ts < Cutoff ->
ets:delete(Tab, Key),
Acc;
({Key, {_, Ts}}, Acc) when Ts < Cutoff ->
ets:delete(Tab, Key),
Acc;
(_, Acc) ->
Acc
end,
ok,
Tab
),
ok.
terminate(#state{local_tab = Tab, gossip_pid = GossipPid}) ->
logger:info(
"Distributed backend shutting down, stopping gossip",
#{domain => [seki]}
),
seki_pg_gossip:stop(GossipPid),
ets:delete(Tab),
ok.
%%----------------------------------------------------------------------
%% Internal
%%----------------------------------------------------------------------
ensure_pg_scope(Scope) ->
case pg:start(Scope) of
{ok, _Pid} -> ok;
{error, {already_started, _Pid}} -> ok
end.