Packages
honeydew
1.4.2
1.5.0
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.0
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc7
1.0.0-rc6
1.0.0-rc5
1.0.0-rc4
1.0.0-rc3
1.0.0-rc2
1.0.0-rc1
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
Pluggable local/clusterable job queue focused on safety.
Current section
Files
Jump to
Current section
Files
lib/honeydew/worker.ex
defmodule Honeydew.Worker do
@moduledoc """
A Honeydew Worker, how you specify your jobs.
"""
use GenServer
require Logger
require Honeydew
alias Honeydew.Crash
alias Honeydew.Job
alias Honeydew.JobMonitor
alias Honeydew.Logger, as: HoneydewLogger
alias Honeydew.Queue
alias Honeydew.JobRunner
alias Honeydew.Workers
alias Honeydew.WorkerSupervisor
@type private :: term
@doc """
Invoked when the worker starts up for the first time.
"""
@callback init(args :: term) :: {:ok, state :: private}
@doc """
Invoked when `init/1` returns anything other than `{:ok, state}` or raises an error
"""
@callback init_failed() :: any()
@optional_callbacks init: 1, init_failed: 0
defmodule State do
@moduledoc false
defstruct [:queue,
:queue_pid,
:module,
:has_init_fcn,
:init_args,
:init_retry_secs,
:start_opts,
:job_runner,
:job,
{:ready, false},
{:private, :no_state}]
end
@doc false
def child_spec([_supervisor, _queue, %{shutdown: shutdown}, _queue_pid] = opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
restart: :transient,
shutdown: shutdown
}
end
@doc false
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
@impl true
@doc false
def init([_supervisor, queue, %{ma: {module, init_args}, init_retry_secs: init_retry_secs}, queue_pid] = start_opts) do
Process.flag(:trap_exit, true)
queue
|> Honeydew.group(Workers)
|> :pg2.join(self())
has_init_fcn =
:functions
|> module.__info__
|> Enum.member?({:init, 1})
{:ok, %State{queue: queue,
queue_pid: queue_pid,
module: module,
init_args: init_args,
init_retry_secs: init_retry_secs,
start_opts: start_opts,
has_init_fcn: has_init_fcn}, {:continue, :module_init}}
end
#
# Internal API
#
@doc false
def run(worker, job, job_monitor), do: GenServer.cast(worker, {:run, %{job | job_monitor: job_monitor}})
@doc false
def module_init(me \\ self()), do: GenServer.cast(me, :module_init)
@doc false
def ready(ready), do: GenServer.cast(self(), {:ready, ready})
@doc false
def job_finished(worker, job), do: GenServer.call(worker, {:job_finished, job})
defp do_module_init(%State{has_init_fcn: false} = state) do
%{state | ready: true} |> send_ready_or_callback
end
defp do_module_init(%State{module: module, init_args: init_args} = state) do
try do
case apply(module, :init, [init_args]) do
{:ok, private} ->
%{state | private: {:state, private}, ready: true}
bad ->
HoneydewLogger.worker_init_crashed(module, Crash.new(:bad_return_value, bad))
%{state | ready: false}
end
rescue e ->
HoneydewLogger.worker_init_crashed(module, Crash.new(:exception, e, System.stacktrace()))
%{state | ready: false}
catch
:exit, reason ->
HoneydewLogger.worker_init_crashed(module, Crash.new(:exit, reason))
%{state | ready: false}
e ->
HoneydewLogger.worker_init_crashed(module, Crash.new(:throw, e, System.stacktrace()))
%{state | ready: false}
end
|> send_ready_or_callback
end
defp send_ready_or_callback(%State{queue_pid: queue_pid, ready: true} = state) do
Honeydew.debug "[Honeydew] Worker #{inspect self()} sending ready"
Process.link(queue_pid)
Queue.worker_ready(queue_pid)
state
end
defp send_ready_or_callback(%State{module: module, init_retry_secs: init_retry_secs} = state) do
:functions
|> module.__info__
|> Enum.member?({:failed_init, 0})
|> if do
module.failed_init
else
Logger.info "[Honeydew] Worker #{inspect self()} re-initing in #{init_retry_secs}s"
:timer.apply_after(init_retry_secs * 1_000, __MODULE__, :module_init, [self()])
end
state
end
defp do_run(%Job{job_monitor: job_monitor} = job, %State{module: module, private: private} = state) do
job = %{job | by: node()}
:ok = JobMonitor.claim(job_monitor, job)
{:ok, job_runner} = JobRunner.run_link(job, module, private)
%{state | job_runner: job_runner, job: job}
end
defp do_job_finished(%Job{result: {:ok, result}, from: from, job_monitor: job_monitor} = job, %State{queue_pid: queue_pid}) do
job = %{job | result: {:ok, result}}
with {owner, _ref} <- from,
do: send(owner, job)
:ok = JobMonitor.job_succeeded(job_monitor)
Process.delete(:job_monitor)
Queue.worker_ready(queue_pid)
:ok
end
defp do_job_finished(%Job{result: {:error, %Crash{} = crash}, job_monitor: job_monitor} = job, _state) do
HoneydewLogger.job_failed(job, crash)
:ok = JobMonitor.job_failed(job_monitor, crash)
Process.delete(:job_monitor)
:error
end
@impl true
@doc false
def handle_continue(:module_init, state) do
{:noreply, do_module_init(state)}
end
@doc false
def handle_continue({:job_finished, job}, state) do
case do_job_finished(job, state) do
:ok ->
{:noreply, state}
:error ->
restart(state)
end
end
@impl true
@doc false
def handle_cast(:module_init, state), do: {:noreply, do_module_init(state)}
def handle_cast({:run, job}, state), do: {:noreply, do_run(job, state)}
@impl true
@doc false
def handle_call({:job_finished, job}, {job_runner, _ref}, %State{job_runner: job_runner} = state) do
Process.unlink(job_runner)
{:reply, :ok, %{state | job_runner: nil, job: nil}, {:continue, {:job_finished, job}}}
end
#
# Our Queue died, our QueueMonitor will stop us soon.
#
@impl true
@doc false
def handle_info({:EXIT, queue_pid, _reason}, %State{queue: queue, queue_pid: queue_pid} = state) do
Logger.warn "[Honeydew] Worker #{inspect queue} (#{inspect self()}) saw its queue die, stopping..."
{:noreply, state}
end
def handle_info({:EXIT, _pid, :normal}, %State{job_runner: nil} = state), do: {:noreply, state}
def handle_info({:EXIT, _pid, :shutdown}, %State{job_runner: nil} = state), do: {:noreply, state}
def handle_info({:EXIT, _pid, {:shutdown, _}}, %State{job_runner: nil} = state), do: {:noreply, state}
def handle_info({:EXIT, job_runner, reason}, %State{job_runner: job_runner, queue: queue, job: job} = state) do
Logger.warn "[Honeydew] Worker #{inspect queue} (#{inspect self()}) saw its job runner (#{inspect job_runner}) die during a job, restarting..."
job = %{job | result: {:error, Crash.new(:exit, reason)}}
{:noreply, state, {:continue, {:job_finished, job}}}
end
def handle_info(msg, %State{queue: queue} = state) do
Logger.warn "[Honeydew] Worker #{inspect queue} (#{inspect self()}) received unexpected message #{inspect msg}, restarting..."
restart(state)
end
defp restart(%State{start_opts: start_opts} = state) do
WorkerSupervisor.start_worker(start_opts)
{:stop, :normal, state}
end
end