Current section

Files

Jump to
honeydew lib honeydew.ex
Raw

lib/honeydew.ex

defmodule Honeydew do
alias Honeydew.Job
require Logger
#
# 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
@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, you may also provide init/1 args: {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. `args` defaults to `[]`.
- `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, `args` defaults to `[]`.
- `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, []})`
"""
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
success_mode =
case opts[:success_mode] do
nil -> nil
{module, args} -> {module, args}
module when is_atom(module) -> {module, []}
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
@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 init/1 args: {module, args}
You can provide any of the following `opts`:
- `num`: the number of workers to start
- `init_retry`: the amount of time, in milliseconds, to wait before respawning a worker who's `init/1` function failed
- `shutdown`: if a worker is in the middle of a job, the amount of time, in milliseconds, to wait before brutally killing it.
- `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)
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])`
"""
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