Packages
honeydew
0.0.4
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/work_queue.ex
defmodule Honeydew.WorkQueue do
use GenServer
require Logger
alias Honeydew.Job
# after max_failures, delay the job by delay_secs
defmodule State do
defstruct max_failures: nil,
delay_secs: nil,
queue: :queue.new, # jobs waiting to be taken by a worker
backlog: HashSet.new, # jobs that have failed max_failures number of times, and are waiting to be re-queued after delay_secs
waiting: :queue.new, # workers that are waiting for a job
working: HashDict.new # workers that are currently working mapped to their current jobs
end
def start_link(name, max_failures, delay_secs) do
# used for random ids for job backlog set (otherwise some jobs would be seen as identical and not added to the set)
:random.seed(:erlang.now)
GenServer.start_link(__MODULE__, %State{max_failures: max_failures, delay_secs: delay_secs}, name: name)
end
#
# Messaging
#
def handle_cast({:add_task, task}, state) do
job = %Job{task: task}
{:noreply, add_job(job, state)}
end
def handle_cast(_msg, state), do: {:noreply, state}
def handle_call({:add_task, task}, from, state) do
job = %Job{task: task, from: from}
{:noreply, add_job(job, state)}
end
def handle_call(:job_please, {worker, _msg_ref} = from, state) do
case :queue.out(state.queue) do
# there's a job in the queue, honey do it, please!
{{:value, job}, queue} ->
{:reply, job, %{state | queue: queue, working: Dict.put(state.working, worker, job)}}
# nothing for the worker to do right now, we'll get back to them later when something arrives
{:empty, _} ->
{:noreply, %{state | waiting: :queue.in(from, state.waiting), working: Dict.delete(state.working, worker)}}
end
end
def handle_call(:monitor_me, {worker, _msg_ref}, state) do
Process.monitor(worker)
{:reply, :ok, state}
end
def handle_call(:status, _from, state) do
%State{queue: queue, backlog: backlog, working: working, waiting: waiting} = state
status = %{
queue: :queue.len(queue),
backlog: Set.size(backlog),
working: Dict.size(working),
waiting: :queue.len(waiting)
}
{:reply, status, state}
end
def handle_call(_msg, _from, state), do: {:reply, :ok, state}
# A worker has died, put its job back on the queue and increment the job's "failures" count
def handle_info({:DOWN, _ref, _type, worker_pid, _reason}, state) do
case Dict.pop(state.working, worker_pid) do
# worker wasn't working on anything
{nil, _working} -> nil
{job, working} ->
state = %{state | working: working}
job = %{job | failures: job.failures + 1}
state = if job.failures < state.max_failures do
add_job(job, state)
else
# Logger.warn "[Honeydew] #{state.worker_module} Job failed too many times, delaying #{state.delay_secs}s: #{inspect job}"
delay_job(job, state)
end
end
{:noreply, state}
end
# delay_secs has elapsed and a failing job is ready to be tried again
def handle_info({:enqueue_delayed_job, job}, state) do
Logger.info "[Honeydew] [#{__MODULE__}] Enqueuing delayed job: #{inspect job}"
state = %{state | backlog: Set.delete(state.backlog, job)}
{:noreply, add_job(job, state)}
end
def handle_info(_msg, state), do: {:noreply, state}
defp add_job(job, state) do
case next_alive_worker(state.waiting) do
# no workers are waiting, add the job to the backlog
{nil, waiting} ->
%{state | queue: :queue.in(job, state.queue), waiting: waiting}
# there's a worker waiting, give them the job
{from_worker, waiting} ->
{worker, _msg_ref} = from_worker
GenServer.reply(from_worker, job)
%{state | waiting: waiting, working: Dict.put(state.working, worker, job)}
end
end
defp delay_job(job, state) do
# random ids are needed so the backlog HashSet sees all jobs as unique
delay_id = [:random.uniform(100_000), :random.uniform(100_000)] ++ Tuple.to_list(:erlang.now)
job = %{job | id: delay_id}
:erlang.send_after(state.delay_secs * 1000, self, {:enqueue_delayed_job, job})
%{state | backlog: Set.put(state.backlog, job)}
end
defp next_alive_worker(waiting) do
case :queue.out(waiting) do
{{:value, from_worker}, waiting} ->
{worker, _msg_ref} = from_worker
if Process.alive? worker do
{from_worker, waiting}
else
next_alive_worker(waiting)
end
{:empty, _} ->
{nil, waiting}
end
end
end