Packages
honeydew
1.0.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/queue/mnesia.ex
defmodule Honeydew.Queue.Mnesia do
use Honeydew.Queue
require Honeydew.Job
alias Honeydew.Job
alias Honeydew.Queue.State
# private queue state
defmodule PState do
defstruct [:table, :access_context]
end
# TODO: document. :(
@pending_match_spec [{Job.job(private: {false, :_}, _: :_) |> Job.to_record(:_), [], [:"$_"]}]
def init(queue_name, [nodes, table_opts, opts]) do
access_context = Keyword.get(opts, :access_context, :sync_transaction)
case :mnesia.create_schema(nodes) do
:ok -> :ok
{:error, {_, {:already_exists, _}}} -> :ok
end
# assert that mnesia started correctly everywhere?
:rpc.multicall(nodes, :mnesia, :start, [])
table_def =
table_opts
|> Keyword.put(:type, :ordered_set)
|> Keyword.put(:attributes, Job.fields)
# inspect/1 here becase queue_name can be of the form {:global, poolname}
table = ["honeydew", inspect(queue_name)] |> Enum.join("_") |> String.to_atom
case :mnesia.create_table(table, table_def) do
{:atomic, :ok} -> :ok
{:aborted, {:already_exists, ^table}} -> :ok
end
:ok = :mnesia.wait_for_tables([table], 15_000)
:ok = reset_after_crash(table, access_context)
{:ok, %PState{table: table, access_context: access_context}}
end
#
# Enqueue/Reserve
#
def enqueue(job, %State{private: %PState{table: table, access_context: access_context}} = state) do
job = %{job | private: {false, :erlang.unique_integer([:monotonic])}} # {in_progress, :id}
:mnesia.activity(access_context, fn ->
:ok =
job
|> Job.to_record(table)
|> :mnesia.write
end)
{state, job}
end
# should the recursion be outside of the transaction?
def reserve(%State{private: %PState{table: table, access_context: access_context}} = state) do
:mnesia.activity(access_context, fn ->
case :mnesia.select(table, @pending_match_spec, 1, :read) do
:"$end_of_table" -> nil
{[job], _cont} ->
{_, id} = Job.job(job, :private)
:ok = :mnesia.delete_object(job)
job = Job.job(job, private: {node(), id})
:ok = :mnesia.write(job)
{state, Job.from_record(job)}
end
end)
end
#
# Ack/Nack
#
def ack(%Job{private: private}, %State{private: %PState{table: table}} = state) do
:ok = :mnesia.dirty_delete(table, private)
state
end
def nack(%Job{private: {_, id}, failure_private: failure_private} = job, %State{private: %PState{table: table,
access_context: access_context}} = state) do
:mnesia.activity(access_context, fn ->
:ok =
%{job | private: {false, id}, failure_private: failure_private}
|> Job.to_record(table)
|> :mnesia.write
:ok = :mnesia.delete({table, {node(), id}})
end)
state
end
#
# Helpers
#
# foldl might be too heavy...
def status(%PState{table: table}) do
{pending, in_progress} =
:mnesia.activity(:async_dirty, fn ->
:mnesia.foldl(fn job, {pending, in_progress} ->
job
|> Job.to_record
|> case do
Job.job(private: {false, _}) ->
{pending + 1, in_progress}
Job.job(private: {_node, _}) ->
{pending, in_progress + 1}
end
end, {0, 0}, table)
end)
%{
mnesia: :mnesia.table_info(table, :all),
count: pending + in_progress,
in_progress: in_progress
}
end
def filter(%PState{table: table, access_context: access_context}, map) when is_map(map) do
:mnesia.activity(access_context, fn ->
map
|> Job.match_spec(table)
|> :mnesia.match_object
|> Enum.map(&Job.from_record/1)
end)
end
def filter(%PState{table: table, access_context: access_context}, function) do
:mnesia.activity(access_context, fn ->
:mnesia.foldl(fn job, list ->
job
|> Job.from_record
|> function.()
|> case do
true -> [job | list]
false -> list
end
end, [], table)
|> Enum.map(&Job.from_record/1)
end)
end
def cancel(%Job{private: {_, id}, }, %PState{table: table, access_context: access_context} = queue) do
reply =
:mnesia.activity(access_context, fn ->
Job.job(private: {:_, id}, _: :_)
|> Job.to_record(table)
|> :mnesia.match_object
|> Enum.map(&Job.to_record/1)
|> case do
[] -> nil
[Job.job(private: {false, id})] ->
:ok = :mnesia.delete({table, {false, id}})
[Job.job(private: {_node, _id})] ->
{:error, :in_progress}
end
end)
{reply, queue}
end
defp reset_after_crash(table, access_context) do
:mnesia.activity(access_context, fn ->
table
|> :mnesia.select(in_progress_match_spec(), :read)
|> Enum.each(fn job ->
{_, id} = Job.job(job, :private)
:ok = :mnesia.delete_object(job)
job = Job.job(job, private: {false, id})
:ok = :mnesia.write(job)
end)
end)
end
defp in_progress_match_spec do
[{Job.job(private: {node(), :_}, _: :_) |> Job.to_record(:_), [], [:"$_"]}]
end
end