defmodule Limiter do
use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
def allow(limiter, key), do: GenServer.call(limiter, {:allow, key})
@impl true
def init(opts) do
clock = Keyword.get(opts, :clock, fn -> System.monotonic_time(:millisecond) end)
{:ok, %{opts: opts, clock: clock, buckets: %{}}}
end
@impl true
def handle_call({:allow, key}, _from, state) do
now = state.clock.()
bucket =
Map.get_lazy(state.buckets, key, fn ->
TokenBucket.new(state.opts[:capacity], state.opts[:interval], now)
end)
case TokenBucket.take(bucket, now) do
{:ok, bucket} -> {:reply, :ok, put_in(state.buckets[key], bucket)}
{:error, reason, bucket} -> {:reply, {:error, reason}, put_in(state.buckets[key], bucket)}
end
end
end
{:ok, clock} = Agent.start_link(fn -> 0 end)
fake_clock = fn -> Agent.get(clock, & &1) end
{:ok, limiter} = Limiter.start_link(capacity: 2, interval: 1_000, clock: fake_clock)