Rexsilient.Decorator (Rexsilient v1.0.0)

Copy Markdown View Source

Runs a function through a circuit breaker, a retry, a fallback or a policy by annotating it.

Instead of wrapping every call in Rexsilient.CircuitBreaker.run/3, a function can be marked with @circuit_breaker so that all its calls go through the circuit breaker:

defmodule MyApp.Payments do
  use Rexsilient.Decorator

  @circuit_breaker :payments
  def charge(order) do
    PaymentsClient.charge(order)
  end
end

MyApp.Payments.charge(order) now behaves like:

Rexsilient.CircuitBreaker.run(:payments, fn -> PaymentsClient.charge(order) end)

The circuit breaker must be declared with Rexsilient and registered as usual; see Rexsilient.CircuitBreaker for its results and how failures are counted.

@retry and @fallback work the same way, with Rexsilient.Retry.run/2 and Rexsilient.Fallback.run/3. The fallback function is called with the decorated function's arguments.

Options

To pass options to Rexsilient.CircuitBreaker.run/3, give a keyword list with the circuit breaker's name under :name:

@circuit_breaker name: :prices, on_open: {:error, :unavailable}
def fetch_price(sku) do
  PricesClient.fetch(sku)
end

@retry, @fallback and @policy take only a name.

Stacking

@fallback, @retry and @circuit_breaker can be used on the same function. They always nest in the same order, whatever the order they are written in:

@fallback :prices
@retry :prices
@circuit_breaker :prices
def fetch_price(sku) do
  PricesClient.fetch(sku)
end

behaves like:

Rexsilient.Fallback.run(:prices, fn sku ->
  Rexsilient.Retry.run(:prices, fn ->
    Rexsilient.CircuitBreaker.run(:prices, fn -> PricesClient.fetch(sku) end)
  end)
end, sku)

Every attempt goes through the breaker and is counted. The retry stops as soon as the breaker is open, and the fallback replaces whatever outcome is left: the last attempt's failure or the breaker's rejection.

When the same stack is used on several functions, declare it once as a policy with Rexsilient.policy/2 and use @policy:

# in the module that uses Rexsilient
policy :prices do
  fallback :prices
  retry :prices
  circuit_breaker :prices
end

# in the decorated module
@policy :prices
def fetch_price(sku) do
  PricesClient.fetch(sku)
end

A policy nests in the same order. @policy cannot be combined with the other decorators on the same function.

For another order, or the same pattern twice, decorate one function per layer, each calling the next:

# The breaker counts a whole retry sequence as one call.
@circuit_breaker :prices
def fetch_price(sku), do: fetch_price_with_retry(sku)

@doc false
@retry :prices
def fetch_price_with_retry(sku), do: PricesClient.fetch(sku)

The inner function must be public, so calling it directly skips the outer layer.

Rules

  • The attribute goes right before the function's first clause and applies to every clause of that name and arity.

  • Only public functions (def) can be decorated. Using it on defp or a macro fails compilation, and so does an attribute that is not followed by a function.

  • Recursive calls from inside the function go through the decorators too.