Packages

Elixir task library with retry, success/failure callback and timeout

Current section

Files

Jump to
tusk lib tusk.ex
Raw

lib/tusk.ex

defmodule Tusk do
use GenServer, restart: :transient
defstruct task: nil, companion: nil, no_retry_exceptions: []
@type task :: {module, atom, [any]} | fun
@type callback :: task
@type option ::
{:no_retry_exceptions, [module]}
| {:on_success, callback}
| {:on_failure, callback}
| {:on_error, callback}
| {:timeout, non_neg_integer | nil | :infinity}
| Supervisor.init_option()
@doc """
Starts a supervisor, a Tusk GenServer and a companion
The supervisor and the companion are supervised by the dynamic supervisor,
and the Tusk GenServer is supervised by the supervisor and monitored by the
companion
"""
@spec start(Supervisor.supervisor(), task, [option]) :: DynamicSupervisor.on_start_child()
def start(sup, task, options \\ []) do
Tusk.DynamicSupervisor.start_child(sup, [task: task] ++ options)
end
@doc """
execute an mfa or a closure
"""
def execute(nil), do: nil
def execute(task, result \\ nil)
def execute(nil, _), do: nil
def execute({m, f, a}, result) when is_atom(m) and is_atom(f) do
apply(m, f, ((is_nil(result) && []) || [result]) ++ a)
end
def execute(func, result) when is_function(func) do
case result do
nil -> func.()
result -> func.(result)
end
end
@spec start_link([option]) :: GenServer.on_start()
def start_link(options) do
GenServer.start_link(__MODULE__, options)
end
@impl GenServer
def init(options) do
companion = Keyword.fetch!(options, :companion)
GenServer.call(companion, :task)
send(self(), :execute)
{:ok, struct(__MODULE__, options)}
end
require Logger
@impl GenServer
def handle_info(:execute, %{companion: companion} = state) do
try do
result = execute(state.task)
GenServer.call(companion, {:ok, result})
Logger.debug("done")
{:stop, :normal, state}
rescue
e ->
if e.__struct__ in state.no_retry_exceptions do
{:stop, {:shutdown, {:no_retry_exception, e}}, state}
else
stacktrace = System.stacktrace()
reraise e, stacktrace
end
end
end
@impl GenServer
def terminate(:normal, _state) do
Logger.debug("Task finished")
end
@impl GenServer
def terminate({:shutdown, {:no_retry_exception, error}}, %{companion: companion}) do
GenServer.call(companion, {:error, {:no_retry_exception, error}})
Logger.debug("exception but no retry")
end
@impl GenServer
def terminate({_error, _stacktrace} = reason, _state) do
Logger.debug(inspect(reason))
end
end