defmodule TaskBunny.Queue do @moduledoc """ Convenience functions for accessing TaskBunny queues. It's a semi private module normally wrapped by other modules. ## Sub Queues When TaskBunny creates(declares) a queue on RabbitMQ, it also creates the following sub queues. - [queue-name].scheduled: holds jobs to be executed in the future - [queue-name].retry: holds jobs to be retried - [queue-name].rejected: holds jobs that were rejected (failed more than max retry times or wrong message format) """ @doc """ Declares a queue with sub queues. Queue.declare_with_subqueues(:default, "normal_jobs") For this call, the function creates(declares) three queues: - normal_jobs: a queue that holds jobs to process - normal_jobs.scheduled: a queue that holds jobs to process in the future - normal_jobs.retry: a queue that holds jobs failed and waiting to retry - normal_jobs.rejected: a queue that holds jobs failed and won't be retried """ @spec declare_with_subqueues(%AMQP.Connection{} | atom, String.t()) :: {map, map, map, map} def declare_with_subqueues(host, work_queue) when is_atom(host) do conn = TaskBunny.Connection.get_connection!(host) declare_with_subqueues(conn, work_queue) end def declare_with_subqueues(connection, work_queue) do {:ok, channel} = AMQP.Channel.open(connection) scheduled_queue = scheduled_queue(work_queue) retry_queue = retry_queue(work_queue) rejected_queue = rejected_queue(work_queue) work = declare(channel, work_queue, durable: true) rejected = declare(channel, rejected_queue, durable: true) # Set main queue as dead letter exchange of retry queue. # It will requeue the message once message TTL is over. retry_options = [ arguments: [ {"x-dead-letter-exchange", :longstr, ""}, {"x-dead-letter-routing-key", :longstr, work_queue} ], durable: true ] retry = declare(channel, retry_queue, retry_options) scheduled_options = [ arguments: [ {"x-dead-letter-exchange", :longstr, ""}, {"x-dead-letter-routing-key", :longstr, work_queue} ], durable: true ] scheduled = declare(channel, scheduled_queue, scheduled_options) AMQP.Channel.close(channel) {work, retry, rejected, scheduled} end @doc """ Deletes the queue and its subqueues. """ @spec delete_with_subqueues(%AMQP.Connection{} | atom, String.t()) :: :ok def delete_with_subqueues(host, work_queue) when is_atom(host) do conn = TaskBunny.Connection.get_connection!(host) delete_with_subqueues(conn, work_queue) end def delete_with_subqueues(connection, work_queue) do {:ok, channel} = AMQP.Channel.open(connection) work_queue |> queue_with_subqueues() |> Enum.each(fn queue -> AMQP.Queue.delete(channel, queue) end) AMQP.Channel.close(channel) :ok end @doc false # Declares a single queue with the options @spec declare(%AMQP.Channel{}, String.t(), keyword) :: map def declare(channel, queue, options \\ []) do options = options ++ [durable: true] {:ok, state} = AMQP.Queue.declare(channel, queue, options) state end @doc """ Returns the message count and consumer count for the given queue. """ @spec state(%AMQP.Connection{} | atom, String.t()) :: map def state(host_or_conn \\ :default, queue) def state(host, queue) when is_atom(host) do conn = TaskBunny.Connection.get_connection!(host) state(conn, queue) end def state(connection, queue) do {:ok, channel} = AMQP.Channel.open(connection) {:ok, state} = AMQP.Queue.status(channel, queue) AMQP.Channel.close(channel) state end @doc """ Returns a list that contains the queue and its subqueue. """ @spec queue_with_subqueues(String.t()) :: [String.t()] def queue_with_subqueues(work_queue) do [work_queue] ++ subqueues(work_queue) end @doc """ Returns all subqueues for the work queue. """ @spec subqueues(String.t()) :: [String.t()] def subqueues(work_queue) do [ retry_queue(work_queue), rejected_queue(work_queue), scheduled_queue(work_queue) ] end @doc """ Returns a name of retry queue. """ @spec retry_queue(String.t()) :: String.t() def retry_queue(work_queue) do work_queue <> ".retry" end @doc """ Returns a name of rejected queue. """ @spec rejected_queue(String.t()) :: String.t() def rejected_queue(work_queue) do work_queue <> ".rejected" end @doc """ Returns a name of scheduled queue. """ @spec scheduled_queue(String.t()) :: String.t() def scheduled_queue(work_queue) do work_queue <> ".scheduled" end end