To implement a distributed, eventually consistent rate limiter across a cluster, you can combine Hammer with an ETS backend and use Phoenix.PubSub to broadcast hits.
Architecture
- Local Counting: Use a module that calls
use Hammer, backend: :ets to manage counters in local memory via ETS. - Broadcasting: When a hit occurs, broadcast an
:inc message containing the key, scale, and increment value to a shared PubSub topic. - Synchronization: Run a
GenServer (Listener) on every node that subscribes to the PubSub topic. When it receives an :inc message, it calls the local Hammer module to increment the counter, ensuring all nodes eventually reflect the same hit count. - Supervision: Wrap both the local Hammer module and the Listener process in a supervisor to manage their lifecycles.
defmodule MyApp.RateLimit do
# 1. The local Hammer module using ETS
defmodule Local do
use Hammer, backend: :ets
end
# 2. The Listener that synchronizes remote hits
defmodule Listener do
use GenServer
def start_link(opts) do
pubsub = Keyword.fetch!(opts, :pubsub)
topic = Keyword.fetch!(opts, :topic)
GenServer.start_link(__MODULE__, {pubsub, topic})
end
def init({pubsub, topic}) do
:ok = Phoenix.PubSub.subscribe(pubsub, topic)
{:ok, []}
end
def handle_info({:inc, key, scale, increment}, state) do
_count = Local.inc(key, scale, increment)
{:noreply, state}
end
end
# 3. The main interface
def hit(key, scale, limit, increment \ 1) do
:ok = broadcast({:inc, key, scale, increment})
Local.hit(key, scale, limit, increment)
end
# 4. Supervision setup
def start_link(opts) do
children = [{Local, opts}, {Listener, pubsub: @pubsub, topic: @topic}]
Supervisor.start_link(children, strategy: :one_for_one)
end
end