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:

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.