defmodule Hammer do @moduledoc """ Hammer is a rate-limiting library for Elixir. It provides a simple way for creating rate limiters, and comes with a built-in ETS backend. defmodule MyApp.RateLimit do use Hammer, backend: :ets end # Start the rate limiter, in case of ETS it will create the ETS table and schedule cleanups MyApp.RateLimit.start_link(clean_period: :timer.minutes(10)) # Check the rate limit allowing 10 requests per second MyApp.RateLimit.hit("some-key", _scale = :timer.seconds(1), _limit = 10) """ @type key :: term @type scale :: pos_integer @type limit :: pos_integer @type count :: pos_integer @type increment :: non_neg_integer @doc """ Checks if a key is allowed to perform an action, and increment the counter. Same as `hit/4` with `increment` set to 1. """ @callback hit(key, scale, limit) :: {:allow, count} | {:deny, timeout} @doc """ Optional callback to check if a key is allowed to perform an action, and increment the counter. Returns `{:allow, count}` if the action is allowed, or `{:deny, timeout}` if the action is denied. This is the only required callback. """ @callback hit(key, scale, limit, increment) :: {:allow, count} | {:deny, timeout} @doc """ Same as `inc/3` with `increment` set to 1. """ @callback inc(key, scale) :: count @doc """ Optional callback for incrementing a counter value for a kit without performing limit check. Returns the new counter value. """ @callback inc(key, scale, increment) :: count @doc """ Optional callback for setting the counter value for a key. Returns the new counter value. """ @callback set(key, scale, count) :: count @doc """ Optional callback for getting the counter value for a key. Returns the current counter value. """ @callback get(key, scale) :: count @optional_callbacks hit: 4, inc: 2, inc: 3, set: 3, get: 2 @doc """ Use the Hammer library in a module to create a rate limiter. defmodule MyApp.RateLimit do use Hammer, backend: :ets end """ defmacro __using__(opts) do quote bind_quoted: [opts: opts] do @behaviour Hammer @hammer_opts opts backend = Keyword.get(opts, :backend) || raise ArgumentError, """ Hammer requires a backend to be specified. Example: use Hammer, backend: :ets """ # this allows :ets to be aliased to Hammer.ETS backend = case backend do :ets -> Hammer.ETS :atomic -> Hammer.Atomic backend -> backend end @before_compile backend end end end