Rexsilient
Rexsilient is a resilience toolkit for Elixir. It provides patterns such as circuit breakers that let an application handle failing dependencies gracefully, instead of waiting on them or making them worse.
Rexsilient is in early development. The circuit breaker is the first pattern available; the API may change before the first stable release.
Installation
Add :rexsilient to your dependencies in mix.exs:
def deps do
[
{:rexsilient, "~> 0.1"}
]
end
Example
Breakers are declared in a module that uses Rexsilient:
defmodule MyApp.Breakers do
use Rexsilient
circuit_breaker :payments do
threshold 5
timeout :timer.seconds(30)
end
circuit_breaker :search do
threshold 10
timeout :timer.seconds(5)
end
end
The breaker manager is added to your supervision tree, before the processes that use the breakers:
children = [
{Rexsilient.CircuitBreaker, modules: [MyApp.Breakers]},
MyAppWeb.Endpoint
]
Calls to the dependency then go through the breaker:
alias Rexsilient.CircuitBreaker
case CircuitBreaker.run(:payments, fn -> Payments.charge(order) end) do
{:ok, receipt} -> {:ok, receipt}
{:error, :circuit_open} -> {:error, :payments_unavailable}
{:error, reason} -> {:error, reason}
end
Usage
Circuit breaker
A circuit breaker watches the calls made to a dependency. After threshold
failures, it opens: calls are rejected immediately, without reaching the
dependency. After timeout milliseconds, it lets a single call through as a
probe. If the probe succeeds, the breaker closes and traffic resumes. If it
fails, the breaker opens again for another timeout.
run/3 returns the function's result unchanged. When the breaker is open, it
returns {:error, :circuit_open} and the function is not called. The rejection
value can be changed:
# a fixed value
CircuitBreaker.run(:payments, fun, on_open: {:error, :unavailable})
# a function, called only when the call is rejected
CircuitBreaker.run(:prices, fn -> Prices.fetch(sku) end,
fallback: fn -> Cache.last_price(sku) end
)
A call counts as a failure when the function returns :error or
{:error, _}, or when it raises, throws or exits. Exceptions are re-raised
unchanged after being recorded, so the breaker is transparent to the caller.
Every other return value counts as a success.
The breaker's state can be inspected with status/1:
CircuitBreaker.status(:payments)
#=> :closed | :open | :half_open | :probing
Design
Checking a breaker is a single atomic read in the calling process, with no message to any process, so it scales with the number of schedulers. A single manager process owns timers and state changes for every breaker. If it crashes, it restarts every breaker closed, so a failure of the breaker itself never blocks traffic.
Patterns
| Pattern | Purpose | Status |
|---|---|---|
| Circuit breaker | Stop calling a dependency that keeps failing | In development |
| Fallback | Provide an alternative result when a call is rejected | Partial, through fallback: |
| Retry | Retry transient failures, with backoff | Planned |
| Timeout | Give up on calls that take too long | Planned |
| Bulkhead | Limit concurrent calls to a dependency | Planned |
| Rate limiter | Limit how often a dependency is called | Planned |
Running tests
$ mix deps.get
$ mix test