Packages
honeydew
1.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.ex
defmodule Honeydew do
alias Honeydew.Job
require Logger
@type mod_or_mod_args :: module | {module, args :: term}
@type queue_name :: String.t | atom | {:global, String.t | atom}
@type supervisor_opts :: Keyword.t
#
# Parts of this module were lovingly stolen from
# https://github.com/elixir-lang/elixir/blob/v1.3.2/lib/elixir/lib/task.ex#L320
#
def async(task, queue, opts \\ [])
def async(task, queue, reply: true) do
{:ok, job} =
task
|> Job.new(queue)
|> struct(from: {self(), make_ref()})
|> enqueue
job
end
def async(task, queue, _opts) do
{:ok, job} =
task
|> Job.new(queue)
|> enqueue
job
end
def yield(job, timeout \\ 5000)
def yield(%Job{from: nil} = job, _), do: raise ArgumentError, reply_not_requested_error(job)
def yield(%Job{from: {owner, _}} = job, _) when owner != self(), do: raise ArgumentError, invalid_owner_error(job)
def yield(%Job{from: {_, ref}}, timeout) do
receive do
%Job{from: {_, ^ref}, result: result} ->
result # may be {:ok, term} or {:exit, term}
after
timeout ->
nil
end
end
def suspend(queue) do
queue
|> get_all_members(:queues)
|> Enum.each(&GenServer.cast(&1, :suspend))
end
def resume(queue) do
queue
|> get_all_members(:queues)
|> Enum.each(&GenServer.cast(&1, :resume))
end
def status(queue) do
queue_status =
queue
|> get_queue
|> GenServer.call(:status)
busy_workers =
queue_status
|> Map.get(:monitors)
|> Enum.map(fn monitor ->
try do
GenServer.call(monitor, :status)
catch
# the monitor may have shut down
:exit, _ -> nil
end
end)
|> Enum.reject(&(!&1))
|> Enum.into(%{})
workers =
queue
|> get_all_members(:workers)
|> Enum.map(&{&1, nil})
|> Enum.into(%{})
|> Map.merge(busy_workers)
%{queue: Map.delete(queue_status, :monitors), workers: workers}
end
def filter(queue, function) do
{:ok, jobs} =
queue
|> get_queue
|> GenServer.call({:filter, function})
jobs
end
def cancel(%Job{queue: queue} = job) do
queue
|> get_queue
|> GenServer.call({:cancel, job})
end
# FIXME: remove
def state(queue) do
queue
|> get_all_members(:queues)
|> Enum.map(&GenServer.call(&1, :"$honeydew.state"))
end
@doc false
def enqueue(%Job{queue: queue} = job) do
queue
|> get_queue
|> case do
{:error, {:no_such_group, _queue}} -> raise RuntimeError, no_queues_running_error(job)
queue -> GenServer.call(queue, {:enqueue, job})
end
end
@doc false
def invalid_owner_error(job) do
"job #{inspect job} must be queried from the owner but was queried from #{inspect self()}"
end
@doc false
def reply_not_requested_error(job) do
"job #{inspect job} didn't request a reply when enqueued, set `:reply` to `true`, see `async/3`"
end
@doc false
def no_queues_running_error(%Job{queue: {:global, _} = queue} = job) do
"can't enqueue job #{inspect job} because there aren't any queue processes running for the distributed queue `#{inspect queue}, are you connected to the cluster?`"
end
@doc false
def no_queues_running_error(%Job{queue: queue} = job) do
"can't enqueue job #{inspect job} because there aren't any queue processes running for `#{inspect queue}`"
end
@type queue_spec_opt ::
{:queue, mod_or_mod_args} |
{:dispatcher, mod_or_mod_args} |
{:failure_mode, mod_or_mod_args | nil} |
{:success_mode, mod_or_mod_args | nil} |
{:supervisor_opts, supervisor_opts}
@doc """
Creates a supervision spec for a queue.
`name` is how you'll refer to the queue to add a task.
You can provide any of the following `opts`:
- `queue`: is the module that queue will use. Defaults to
`Honeydew.Queue.ErlangQueue`. You may also provide args to the queue's
`c:Honeydew.Queue.init/2` callback using the following format:
`{module, args}`.
- `dispatcher`: the job dispatching strategy, `{module, init_args}`.
- `failure_mode`: the way that failed jobs should be handled. You can pass
either a module, or `{module, args}`. The module must implement the
`Honeydew.FailureMode` behaviour. Defaults to
`{Honeydew.FailureMode.Abandon, []}`.
- `success_mode`: a callback that runs when a job successfully completes. You
can pass either a module, or `{module, args}`. The module must implement
the `Honeydew.SuccessMode` behaviour. Defaults to `nil`.
- `supervisor_opts`: options accepted by `Supervisor.Spec.supervisor/3`.
For example:
- `Honeydew.queue_spec("my_awesome_queue")`
- `Honeydew.queue_spec("my_awesome_queue", queue: {MyQueueModule, [ip: "localhost"]}, dispatcher: {Honeydew.Dispatcher.MRU, []})`
"""
@spec queue_spec(queue_name, [queue_spec_opt]) :: Supervisor.Spec.spec
def queue_spec(name, opts \\ []) do
{module, args} =
case opts[:queue] do
nil -> {Honeydew.Queue.ErlangQueue, []}
module when is_atom(module) -> {module, []}
{module, args} -> {module, args}
end
dispatcher =
opts[:dispatcher] ||
case name do
{:global, _} -> {Honeydew.Dispatcher.LRUNode, []}
_ -> {Honeydew.Dispatcher.LRU, []}
end
# this is intentionally undocumented, i'm not yet sure there's a real use case for multiple queue processes
num = opts[:num] || 1
failure_mode =
case opts[:failure_mode] do
nil -> {Honeydew.FailureMode.Abandon, []}
{module, args} -> {module, args}
module when is_atom(module) -> {module, []}
end
{failure_module, failure_args} = failure_mode
:ok = failure_module.validate_args!(failure_args) # will raise on failure
success_mode =
case opts[:success_mode] do
nil -> nil
{module, args} -> {module, args}
module when is_atom(module) -> {module, []}
end
with {success_module, success_args} <- success_mode do
:ok = success_module.validate_args!(success_args) # will raise on failure
end
supervisor_opts =
opts
|> Keyword.get(:supervisor_opts, [])
|> Keyword.put_new(:id, {:queue, name})
Honeydew.create_groups(name)
Supervisor.Spec.supervisor(
Honeydew.QueueSupervisor,
[name, module, args, num, dispatcher, failure_mode, success_mode],
supervisor_opts)
end
@type worker_spec_opt ::
{:num, non_neg_integer} |
{:init_retry, non_neg_integer} |
{:supervisor_opts, supervisor_opts} |
{:nodes, [node]}
@doc """
Creates a supervision spec for workers.
`queue` is the name of the queue that the workers pull jobs from.
`module` is the module that the workers in your queue will use. You may also
provide `c:Honeydew.Worker.init/1` args with `{module, args}`.
You can provide any of the following `opts`:
- `num`: the number of workers to start. Defaults to `10`.
- `init_retry`: the amount of time, in seconds, to wait before respawning
a worker whose `c:Honeydew.Worker.init/1` function failed. Defaults to `5`.
- `shutdown`: if a worker is in the middle of a job, the amount of time, in
milliseconds, to wait before brutally killing it. Defaults to `10_000`.
- `supervisor_opts` options accepted by `Supervisor.Spec.supervisor/3`.
- `nodes`: for :global queues, you can provide a list of nodes to stay
connected to (your queue node and enqueuing nodes). Defaults to `[]`.
For example:
- `Honeydew.worker_spec("my_awesome_queue", MyJobModule)`
- `Honeydew.worker_spec("my_awesome_queue", {MyJobModule, [key: "secret key"]}, num: 3)`
- `Honeydew.worker_spec({:global, "my_awesome_queue"}, MyJobModule, nodes: [:clientfacing@dax, :queue@dax])`
"""
@spec worker_spec(queue_name, mod_or_mod_args, [worker_spec_opt])
:: Supervisor.Spec.spec
def worker_spec(name, module_and_args, opts \\ []) do
{module, args} =
case module_and_args do
module when is_atom(module) -> {module, []}
{module, args} -> {module, args}
end
num = opts[:num] || 10
init_retry = opts[:init_retry] || 5
shutdown = opts[:shutdown] || 10_000
nodes = opts[:nodes] || []
supervisor_opts =
opts
|> Keyword.get(:supervisor_opts, [])
|> Keyword.put_new(:id, {:worker, name})
Honeydew.create_groups(name)
Supervisor.Spec.supervisor(
Honeydew.WorkerGroupSupervisor,
[name, module, args, num, init_retry, shutdown, nodes],
supervisor_opts)
end
@groups [:workers,
:monitors,
:queues]
Enum.each(@groups, fn group ->
@doc false
def group(queue, unquote(group)) do
name(queue, unquote(group))
end
end)
@supervisors [:worker,
:worker_group,
:node_monitor,
:queue]
Enum.each(@supervisors, fn supervisor ->
@doc false
def supervisor(queue, unquote(supervisor)) do
name(queue, "#{unquote(supervisor)}_supervisor")
end
end)
@doc false
def create_groups(queue) do
Enum.each(@groups, fn name ->
queue |> group(name) |> :pg2.create
end)
end
@doc false
def delete_groups(queue) do
Enum.each(@groups, fn name ->
queue |> group(name) |> :pg2.delete
end)
end
@doc false
def get_all_members({:global, _} = queue, name) do
queue |> group(name) |> :pg2.get_members
end
@doc false
def get_all_members(queue, name) do
get_all_local_members(queue, name)
end
# we need to know local members to shut down local components
@doc false
def get_all_local_members(queue, name) do
queue |> group(name) |> :pg2.get_local_members
end
@doc false
def get_queue({:global, _name} = queue) do
queue |> group(:queues) |> :pg2.get_closest_pid
end
@doc false
def get_queue(queue) do
queue
|> group(:queues)
|> :pg2.get_local_members
|> case do
[] -> nil
members when is_list(members) -> Enum.random(members)
error -> error
end
end
defp name({:global, queue}, component) do
name([:global, queue], component)
end
defp name(queue, component) do
["honeydew", component, queue] |> List.flatten |> Enum.join(".") |> String.to_atom
end
@doc false
defmacro debug(ast) do
quote do
fn ->
Logger.debug unquote(ast)
end
end
end
end