Packages
honeydew
1.5.0
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/queue/mnesia.ex
defmodule Honeydew.Queue.Mnesia do
@moduledoc """
A mnesia-based queue implementation.
This queue is configurable in all the ways mnesia is. For example, you can:
* Run with replication (with queues running on multiple nodes)
* Persist jobs to disk (dets)
"""
require Honeydew.Job
require Logger
require Record
alias Honeydew.Job
alias Honeydew.Queue
alias Honeydew.Queue.State, as: QueueState
alias __MODULE__.WrappedJob
@behaviour Queue
# private queue state
defmodule PState do
@moduledoc false
defstruct [:table,
:in_progress_table,
:access_context]
end
@poll_interval 1_000
@impl true
def validate_args!(opts) do
nodes_list = nodes_list(opts)
if Enum.empty?(nodes_list) do
raise ArgumentError, "You must provide either :ram_copies, :disc_copies or :disc_only_copies as to #{__MODULE__}, for instance `[disc_copies: [node()]]`."
end
Enum.each(nodes_list, fn
node when not is_atom(node) ->
raise ArgumentError, "You provided node name `#{inspect node}` to the #{__MODULE__} queue, node names must be atoms."
_ -> :ok
end)
:ok
end
@impl true
def init(queue_name, table_opts) do
nodes = nodes_list(table_opts)
if on_disk?(table_opts) do
case :mnesia.create_schema(nodes) do
:ok -> :ok
{:error, {_, {:already_exists, _}}} -> :ok
end
end
# assert that mnesia started correctly everywhere?
:rpc.multicall(nodes, :mnesia, :start, [])
generic_table_def =
Keyword.merge(table_opts, [
attributes: WrappedJob.record_fields(),
record_name: WrappedJob.record_name()
])
# inspect/1 here becase queue_name can be of the form {:global, poolname}
table = ["honeydew", inspect(queue_name)] |> Enum.join("_") |> String.to_atom
in_progress_table = ["honeydew", inspect(queue_name), "in_progress"] |> Enum.join("_") |> String.to_atom
tables = %{table => [type: :ordered_set],
in_progress_table => [type: :set]}
Enum.each(tables, fn {name, opts} ->
table_definition = Keyword.merge(generic_table_def, opts)
case :mnesia.create_table(name, table_definition) do
{:atomic, :ok} ->
:ok
{:aborted, {:already_exists, ^name}} ->
:ok
end
end)
:ok =
tables
|> Map.keys()
|> :mnesia.wait_for_tables(15_000)
state = %PState{table: table,
in_progress_table: in_progress_table,
access_context: access_context(table_opts)}
:ok = reset_after_crash(state)
time_warp_mode_warning()
poll()
{:ok, state}
end
#
# Enqueue/Reserve
#
@impl true
def enqueue(job, %PState{table: table} = state) do
wrapped_job = WrappedJob.new(job)
wrapped_job_record = WrappedJob.to_record(wrapped_job)
:ok = :mnesia.dirty_write(table, wrapped_job_record)
{state, wrapped_job.job}
end
@impl true
def reserve(%PState{table: table, access_context: access_context} = state) do
:mnesia.activity(access_context, fn ->
table
|> :mnesia.select(WrappedJob.reserve_match_spec(), 1, :read)
|> case do
:"$end_of_table" ->
{:empty, state}
{[wrapped_job_record], _cont} ->
:ok = move_to_in_progress_table(wrapped_job_record, state)
%WrappedJob{job: job} = WrappedJob.from_record(wrapped_job_record)
{job, state}
end
end)
end
#
# Ack/Nack
#
@impl true
def ack(%Job{private: id}, %PState{in_progress_table: in_progress_table, access_context: access_context} = state) do
pattern = WrappedJob.id_pattern(id)
:mnesia.activity(access_context, fn ->
[wrapped_job] = :mnesia.match_object(in_progress_table, pattern, :read)
:ok = :mnesia.delete_object(in_progress_table, wrapped_job, :write)
end)
state
end
@impl true
def nack(%Job{private: id, failure_private: failure_private, delay_secs: delay_secs}, %PState{} = state) do
move_to_pending_table(id, %{failure_private: failure_private, delay_secs: delay_secs}, state)
state
end
#
# Helpers
#
@impl true
def status(%PState{table: table, in_progress_table: in_progress_table}) do
mnesia_info = %{
table => :mnesia.table_info(table, :all),
in_progress_table => :mnesia.table_info(in_progress_table, :all)
}
num_pending = mnesia_info[table][:size]
num_in_progress = mnesia_info[in_progress_table][:size]
%{
mnesia: mnesia_info,
count: num_pending + num_in_progress,
in_progress: num_in_progress
}
end
@impl true
def filter(%PState{table: table, access_context: access_context}, map) when is_map(map) do
:mnesia.activity(access_context, fn ->
pattern = WrappedJob.filter_pattern(map)
table
|> :mnesia.match_object(pattern, :read)
|> Enum.map(&WrappedJob.from_record/1)
|> Enum.map(fn %WrappedJob{job: job} -> job end)
end)
end
@impl true
def filter(%PState{table: table, access_context: access_context}, function) do
:mnesia.activity(access_context, fn ->
:mnesia.foldl(fn wrapped_job_record, list ->
%WrappedJob{job: job} = WrappedJob.from_record(wrapped_job_record)
job
|> function.()
|> case do
true -> [job | list]
false -> list
end
end, [], table)
end)
end
@impl true
def cancel(%Job{private: id}, %PState{table: table, in_progress_table: in_progress_table, access_context: access_context} = state) do
reply =
:mnesia.activity(access_context, fn ->
pattern = WrappedJob.id_pattern(id)
table
|> :mnesia.match_object(pattern, :read)
|> case do
[wrapped_job] ->
:ok = :mnesia.delete_object(table, wrapped_job, :write)
[] ->
in_progress_table
|> :mnesia.match_object(pattern, :read)
|> case do
[] ->
{:error, :not_found}
[_wrapped_job] ->
{:error, :in_progress}
end
end
end)
{reply, state}
end
defp reset_after_crash(%PState{in_progress_table: in_progress_table} = state) do
in_progress_table
|> :mnesia.dirty_first()
|> case do
:"$end_of_table" ->
:ok
key ->
key
|> WrappedJob.id_from_key
|> move_to_pending_table(%{}, state)
reset_after_crash(state)
end
:ok
end
defp move_to_in_progress_table(wrapped_job_record, %PState{table: table, in_progress_table: in_progress_table, access_context: access_context}) do
:mnesia.activity(access_context, fn ->
:ok = :mnesia.write(in_progress_table, wrapped_job_record, :write)
:ok = :mnesia.delete({table, WrappedJob.key(wrapped_job_record)})
end)
end
defp move_to_pending_table(id, updates, %PState{table: table, in_progress_table: in_progress_table, access_context: access_context}) do
pattern = WrappedJob.id_pattern(id)
:mnesia.activity(access_context, fn ->
wrapped_job_record =
in_progress_table
|> :mnesia.match_object(pattern, :read)
|> List.first
%WrappedJob{job: job} = WrappedJob.from_record(wrapped_job_record)
updated_wrapped_job_record =
job
|> struct(updates)
|> WrappedJob.new
|> WrappedJob.to_record
:ok = :mnesia.write(table, updated_wrapped_job_record, :write)
:ok = :mnesia.delete_object(in_progress_table, wrapped_job_record, :write)
end)
end
defp access_context(opts) do
if nodes_list(opts) == [node()] do
if on_disk?(opts) do
:async_dirty
else
:ets
end
else
:sync_transaction
end
end
defp nodes(opts) do
ram_copies = Keyword.get(opts, :ram_copies, [])
disc_copies = Keyword.get(opts, :disc_copies, [])
disc_only_copies = Keyword.get(opts, :disc_only_copies, [])
%{
ram: ram_copies,
disc: disc_copies,
disc_only: disc_only_copies
}
end
defp nodes_list(opts) do
opts
|> nodes
|> Map.values
|> List.flatten
end
defp on_disk?(opts) do
nodes = nodes(opts)
!Enum.empty?(nodes[:disc]) || !Enum.empty?(nodes[:disc_only])
end
@impl true
def handle_info(:__poll__, %QueueState{} = queue_state) do
poll()
{:noreply, Queue.dispatch(queue_state)}
end
defp poll do
{:ok, _} = :timer.send_after(@poll_interval, :__poll__)
end
defp time_warp_mode_warning do
if :erlang.system_info(:time_warp_mode) != :multi_time_warp do
Logger.warn "[Honeydew] It's recommended to use the Mnesia queue with the 'multi_time_warp' time correction mode to minimize montonic clock freezes, see http://erlang.org/doc/apps/erts/time_correction.html#multi-time-warp-mode."
end
end
end