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.
-
Add
gentry
to your list of dependencies inmix.exs
:def deps do [{:gentry, "~> 0.1"}] end
-
If you're using Elixir 1.3, ensure
gentry
is started with your application:def application do [applications: [:logger, :gentry]] end
-
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
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
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
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, _}
Gentry only supports exponential doubling for its backoff algorithm.
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.