View Source PlugLimit (PlugLimit v0.1.0)
Rate limiting Plug module based on Redis Lua scripting.
architecture
Architecture
PlugLimit
is a Plug
behaviour module implementation using
Redis Lua scripting to provide
rate limiting functionality.
Module plugs should implement two callback functions: Plug.init/1
and Plug.call/2
.
PlugLimit
init/1
function is responsible for building plug configuration, please refer to the
Configuration section below for details.
PlugLimit
call/2
function has following responsibilities:
- Establish if given request should be allowed or denied in accordance with selected rate limiting strategy.
- Determine values of the rate limiting http headers.
- Set rate limiting http headers for the response.
- Halt request processing pipeline if rate limit was exceeded and send response with
appropriate status code, usually
429 - Too Many Requests
.
Tasks 1 and 2 are performed by evaluation of the Redis Lua script implementing rate limiting
algorithm.
Tasks 3 and 4 are executed by built-in put_response/4
function or user provided equivalent
callback function leveraging Redis Lua script evaluation results.
PlugLimit Redis Lua scripts are loaded to the Redis scripts cache on the first Plug.call/2
callback invocation by using Redis SCRIPT LOAD
command.
Generated by SCRIPT LOAD
SHA1 script hash is cached locally as a :persistent_term
key.
Lua script SHA1 hash is later retrieved from :persistent_term
local cache and used with
Redis EVALSHA
command on subsequent rate-limiter
Lua script evaluations.
Implemented SHA1 Lua script caching mechanism is resilient to Redis script cache resets by Redis
instance reboots or Redis SCRIPT FLUSH
command use.
Redis Lua script evaluation is an atomic operation resilient to the race conditions in distributed environments.
In normal circumstances latency introduced by Redis EVALSHA
command should be close to a single
Redis request/response round trip time, usually less than ~1 ms.
Redis Lua script execution blocks single-threaded Redis server, so it is advised to use a separate standalone Redis instance for PlugLimit rate-limiters, especially when using custom untested Lua script implementations. Data stored by Redis rate-limiters in most cases can be considered as strongly interim so using highly available Redis Sentinel instances for rate-limiters data might be unnecessary. For high traffic volume cases, sharding can be easily achieved by distributing independent Phoenix router pipelines or scopes rate-limiters between dedicated Redis instances.
usage
Usage
PlugLimit in most basic use case requires configuration of a function which will execute Redis commands:
# config/config.exs
config :plug_limit,
enabled?: true,
cmd: {MyApp.Redis, :command, []}
When working with Phoenix Framework PlugLimit
plug call can be placed at the endpoint, router
or controller depending on requirements.
Example of minimal PlugLimit
call used in :high_cost_pipeline
router pipeline:
#lib/my_app_web/router.ex
pipeline :high_cost_pipeline do
plug(PlugLimit, opts: [10, 60], key: {MyApp.RateLimiter, :user_id_key, [:high_cost_pipeline]})
# remaining pipeline plugs...
end
Example above will evaluate request rate-limiting parameters using built-in default Lua script
implementing fixed window algorithm with rate limiter algorithm options given in the :opts
list.
First list element specifies request limit, set to 10 here, second item specifies limiting time
window length in seconds, set to 60.
Redis rate-limiter bucket name will be evaluated with function defined by the :key
MFA tuple.
Example Redis key bucket name function result for user_id=12345
:
{:ok, ["high_cost_pipeline_limiter:12345"]}
.
Example PlugLimit
configuration for built-in token bucket rate-limiter:
#lib/my_app_web/router.ex
pipeline :high_cost_pipeline do
plug(PlugLimit,
limiter: :token_bucket,
opts: [20, 600, 5],
key: {MyApp.RateLimiter, :user_id_key, ["high_cost_pipeline"]}
)
# remaining pipeline plugs...
end
Configuration options details are described in the Configuration section below.
Instead of using generic PlugLimit
module you can use provided convenience wrappers:
PlugLimit.FixedWindow
or PlugLimit.TokenBucket
.
Built-in rate-limiting algorithms are described in the "Redis Lua script rate limiters" LIMITERS.md file.
Unit testing for user applications using PlugLimit library is described in "Unit testing" TESTING.md file.
configuration
Configuration
PlugLimit configuration is built from following sources:
- global
:plug_limit
configuration parameters from application configuration file, usuallyconfig/*.exs
, - parameters overwriting global configuration passed as arguments to the PlugLimit plug call in the application router or controller,
- hard-coded default values.
PlugLimit
is using a concept of rate-limiters to organize individual limiters configurations.
Rate-limiters configurations are declared in application configuration file using :limiters
key. Each rate-limiter must be associated with a valid Lua script. Lua scripts are configured
with :luascripts
key.
Full example configuration defining :custom_limiter
limiter using :custom_bucket
Lua script:
# config/config.exs
config :plug_limit,
limiters: [
custom_limiter: %{
cmd: {MyApp.Redis, :command, ["redis://10.10.10.2:6379/"]},
key: {MyApp.RateLimiter, :user_id_key, []},
log_level: :error,
luascript: :custom_bucket,
response: {MyApp.RateLimiter, :respond, []}
}
],
luascripts: [
custom_bucket: %{
script: {File, :read, ["./lua/custom_bucket.lua"]},
headers: [
"x-ratelimit-limit",
"x-ratelimit-reset",
"x-ratelimit-remaining",
"x-acme-custom-header"
]
}
]
Above defined :custom_limiter
should be referred as follows:
plug(PlugLimit, limiter: :custom_limiter, opts: [20, 600, 5])
application-configuration-options
Application configuration options
Primary part of PlugLimit
configuration is located in the application configuration file,
usually config/config.exs
, under :plug_limit
key. Some of the options defined in application
configuration can be overwritten with plug(PlugLimit, [options...])
call.
Available configuration options:
:enabled?
- when set to Booleanfalse
or string"false"
PlugLimit
is disabled andplug(PlugLimit, opts: [...])
call immediately returns unmodifiedconn
struct. To enablePlugLimit
,:enabled?
key must be set to Booleantrue
or string"true"
. Default:false
.:cmd
- MFA tuple pointing at the user defined two arity function executing Redis commands. As a first argument function will receive a Redis command as a list, for example:["SET", "mykey", "foo"]
. As a second parameter, static argument defined in the MFA tuple will be passed. Redis command function should return{:ok, redis_response}
on success and{:error, reason}
on error. When using Redix library as a client,:cmd
command should be aRedix.command/3
wrapper. When using eredis library, wrapper for the:eredis.q/2,3
should be implemented. Redis command function defined here will be used as a default function for limiters that do not have their own:cmd
specified. Optional if each limiter has its own:cmd
defined, required otherwise.:log_level
- specifies log level for rate-limiters that do not set their own:log_level
. Only library errors are logged - withput_response/4
function. Boolean valuefalse
disables logging. Please refer to theLogger
documentation for valid log levels. Default::error
.:response
- MFA tuple pointing at the user defined 4 arity function providing request response depending on rate-limiter Lua script evaluation results. Please refer to the built-in response functionput_response/4
description for details. Can be overwritten for individual limiters. Default:put_response/4
.:limiters
- keyword list with user provided rate-limiters. See below for details. Optional.:luascripts
- keyword list defining Lua scripts for rate-limiters defined with:limiters
. See below for details. Optional.
PlugLimit :enabled?
option is the only option evaluated at run-time with Plug.call/2
,
so function like System.get_env/2
can be used here.
All other configuration options are initialized with Plug.init/1
, which usually takes place at
compile time for production or release environments and run-time for testing and development.
In a production environment, PlugLimit
:enabled
might be controlled using environmental
variable, for example:
# config/config.exs
config :plug_limit, cmd: {MyApp.Redis, :command, []}
# config/releases.exs
config :plug_limit, enabled?: System.get_env("PLUG_LIMIT_ENABLED", "false")
# config/dev.exs
config :plug_limit, enabled?: true
# config/test.exs
config :plug_limit, enabled?: true
Custom user rate-limiters are configured as a :limiters
keyword list.
Rate-limiters are declared as maps with following keys:
:cmd
- overwrites:cmd
global key for a given rate-limiter. Optional.:key
- MFA tuple pointing at the user defined two arity function providing Redis keys names that will be passed later to the Redis Lua script. Function receives requestPlug.Conn.t()
struct as a first argument and static argument from the MFA tuple as a second argument. Function should return{:ok, [key :: String.t()]}
when successful and{:error, reason}
on error. Function should return especially name of the key that Redis Lua script will use to create a unique bucket for a given rate-limiter and requests group. Please refer to "Redis Lua script rate limiters" LIMITERS.md file for further discussion. Example:key
function implementation:
Redis keys names should follow Redis keys naming conventions.def user_key(%Plug.Conn{assigns: %{user_id: user_id}}, prefix), do: {:ok, [to_string(prefix) <> ":" <> to_string(user_id)]} def user_key(_conn, _prefix), do: {:error, "Missing user_id"}
:key
value can be overwritten in a plug call configuration. Optional if:key
is specified for eachPlugLimit
plug call, required otherwise.:log_level
- overwrites global:log_level
for a given rate-limiter. Optional.:luascript
- atom defining Lua script for a given rate-limiter. Lua scripts are defined with:luascripts
keyword list, see below for details. Required.:response
- overwrites global:response
for a given rate-limiter. Optional.
PlugLimit provides two built-in rate-limiters: :fixed_window
and :token_bucket
, please refer
to "Redis Lua script rate limiters" LIMITERS.md file for details.
Each rate-limiter is associated with Redis Lua script checking if given request should
be allowed or denied and evaluating rate limiting http headers. Redis Lua scripts are configured
as :luascripts
keyword list. Each script is declared as a map with following keys:
:script
- MFA tuple pointing at one arity function returning{:ok, limiter_script :: String.t()}
on success and{:error, reason}
on error. Function receives as an argument static argument from the MFA tuple. Example implementations:
Required.def get_lua_script_by_path(path), do: File.read(path) def my_lua_script(_arg), do: {:ok, "-- Lua script body"}
:headers
- list of rate limiting headers keys to be used with headers values returned by given Redis Lua script to build request response http headers. Example headers list:
Please refer to "Redis Lua script rate limiters" LIMITERS.md file for more detailed discussion on rate limiting headers. Required.headers: [ "x-ratelimit-limit", "x-ratelimit-reset", "x-ratelimit-remaining" ]
plug-call-configuration-options
Plug call configuration options
:key
- MFA tuple, overwrites value given in limiter's application configuration. Required if not provided in the limiter configuration.:limiter
- atom selecting rate limiter. List of built-in limiters is provided in LIMITERS.md file. Default::fixed_window
.:opts
- list with rate limiting options like requests limit, time window length or burst rate. List is passed as an argument to the Redis Lua script, see LIMITERS.md for built-in limiters options. Required.
Link to this section Summary
Functions
Puts new rate limiting http response headers in the connection and halts the Plug pipeline if rate limit was exceeded.
Link to this section Types
Link to this section Functions
@spec put_response( conn :: Plug.Conn.t(), conf :: t(), eval_result :: eval_result(), args :: any() ) :: Plug.Conn.t()
Puts new rate limiting http response headers in the connection and halts the Plug pipeline if rate limit was exceeded.
put_response/4
is a default PlugLimit
function preparing an http response accordingly
with Redis Lua script evaluation results.
Custom response function can be selected by setting :response
global or given limiter
configuration keys.
Function accepts following arguments:
Plug.Conn.t()
connection.- Rate-limiter configuration as a
PlugLimit.t()
struct. - Redis Lua script evaluation result as a
PlugLimit.eval_result()
type. - Static argument given in the
:response
MFA tuple.
Function returns Plug.Conn.t()
struct with rate limiting headers.
If rate limit is exceeded function halts Plug pipeline and sends response with 429
status code
and plain-text body "Too Many Requests"
.
If Redis Lua script evaluation or any other rate-limiting processing function fails,
put_response/4
function will log resulting error with Logger level set by :log_level
configuration setting and return unmodified connection struct.
Custom response functions and custom Redis Lua scripts are described in more details in "Redis Lua script rate limiters" LIMITERS.md file.