/gentry

A supervisor and generic server with configurable retries and a backoff

Primary LanguageElixirMIT LicenseMIT

Gentry

Because failures are a royal pain.

Use Gentry to run tasks with a configurable retry and backoff period.

Gentry will try each task, given as a function. If it fails, Gentry will retry the task retries number of times. The defalt value for retries is 5.

Before running the task after it fails, Gentry will use the configured retry_backoff value as milliseconds. The default value for retry_backoff is 5000.

The computed backoff for each retry is exponentially doubled:

# something like this ...
retry_backoff() * :math.pow(2, retries() - retries_remaining())

So the time between the first try and the first retry is:

   5000 * :math.pow(2, 5 - 5)
=> 5000 * :math.pow(2, 0)
=> 5000 * 1
=> 5000

The time between the first retry and the second retry is twice the retry_backoff time, etc.

Installation

  1. Add gentry to your list of dependencies in mix.exs:

    def deps do
      [{:gentry, "~> 0.1"}]
    end
  2. If you're using Elixir 1.3, ensure gentry is started with your application:

    def application do
      [applications: [:logger, :gentry]]
    end
  3. Configure the Gentry supervisor:

    # ...
    children = [
      # ...
      supervisor(Gentry.Supervisor, []),
    ]
    
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
    # config.exs
    config :gentry,
      retries: 5,
      retry_backoff: 5_000 # milliseconds

Usage

Gentry

Use the Gentry module for easy, synchronous calls with retries.

case Gentry.run_task(fn -> write_to_database(changeset) end) do
  {:ok, _result} ->
    Logger.debug "Successfully processed changeset: #{inspect changeset}"
  {:error, error} ->
    Logger.debug "Failed to process changeset: #{inspect changeset}, because: #{inspect error}"
end

Asynchronous Operation

From within a GenServer, start a new task by running:

{:ok, pid} = Gentry.WorkerSupervisor.start_worker(f, self())

Then handle the result:

def handle_info({:gentry, pid, :ok, result}, state) do
  Logger.debug "Received success from child: #{inspect pid} with result: #{inspect result}"
  {:noreply, state}
end
def handle_info({:gentry, pid, :error, error}, state) do
  Logger.debug "Received error from child: #{inspect pid}"
  {:noreply, state}
end
def handle_info({:gentry, pid, :retry, remaining}, state) do
  Logger.debug "Received retry notification from child: #{inspect pid}, #{remaining} tries remaining"
  {:noreply, state}
end

Failures

For Gentry, a failure is either:

  • A task that has exited abormally (see the description here)
  • A task that returns a value not matching either :ok or {:ok, _}

Limitations

Backoff

Gentry only supports exponential doubling for its backoff algorithm.

Naming

Gentry uses a fixed naming scheme, so multiple instances of the Gentry supervisor is not possible. However, any number of concurrent tasks may be run.