Current section

Files

Jump to
verk lib verk queue_manager.ex
Raw

lib/verk/queue_manager.ex

defmodule Verk.QueueManager do
@moduledoc """
QueueManager interacts with redis to dequeue jobs from the specified queue.
"""
use GenServer
require Logger
alias Verk.RetrySet
alias Verk.DeadSet
@processing_key "processing"
@external_resource "priv/lpop_rpush_src_dest.lua"
@external_resource "priv/mrpop_lpush_src_dest.lua"
@lpop_rpush_src_dest_script_sha Verk.Scripts.sha("lpop_rpush_src_dest")
@mrpop_lpush_src_dest_script_sha Verk.Scripts.sha("mrpop_lpush_src_dest")
@max_retry 25
@max_dead 100
@max_jobs 100
defmodule State do
defstruct [:queue_name, :redis, :node_id]
end
@doc """
Returns the atom that represents the QueueManager of the `queue`
"""
@spec name(binary | atom) :: atom
def name(queue) do
String.to_atom("#{queue}.queue_manager")
end
@doc false
def start_link(name, queue_name) do
GenServer.start_link(__MODULE__, [queue_name], name: name)
end
@doc """
Pop a job from the assigned queue and reply with it if not empty
"""
def dequeue(queue_manager, n, timeout \\ 5000) do
try do
GenServer.call(queue_manager, { :dequeue, n }, timeout)
catch
:exit, { :timeout, _ } -> :timeout
end
end
@doc """
Add job to be retried in the assigned queue
"""
def retry(queue_manager, job, exception, stacktrace, timeout \\ 5000) do
try do
GenServer.call(queue_manager, { :retry, job, Timex.Time.now(:secs), exception, stacktrace }, timeout)
catch
:exit, { :timeout, _ } -> :timeout
end
end
@doc """
Acknowledge that a job was processed
"""
def ack(queue_manager, job) do
GenServer.cast(queue_manager, {:ack, job})
end
@doc """
Enqueue inprogress jobs back to the queue
"""
def enqueue_inprogress(queue_manager) do
GenServer.call(queue_manager, :enqueue_inprogress)
end
@doc """
Connect to redis
"""
def init([queue_name]) do
node_id = Application.get_env(:verk, :node_id, "1")
{ :ok, redis_url } = Application.fetch_env(:verk, :redis_url)
{ :ok, redis } = Redix.start_link(redis_url)
Verk.Scripts.load(redis)
state = %State{ queue_name: queue_name, redis: redis, node_id: node_id }
Logger.info "Queue Manager started for queue #{queue_name}"
{ :ok, state }
end
@doc false
def handle_call(:enqueue_inprogress, _from, state) do
case Redix.command(state.redis, ["EVALSHA", @lpop_rpush_src_dest_script_sha, 2, inprogress(state.queue_name, state.node_id), "queue:#{state.queue_name}"]) do
{ :ok, n } -> Logger.info("#{n} jobs readded to the queue #{state.queue_name} from inprogress list")
{ :reply, :ok, state }
{ :error, reason } -> Logger.error("Failed to add jobs back to queue #{state.queue_name} from inprogress list. Error: #{inspect reason}")
{ :stop, :redis_failed, state }
end
end
def handle_call({ :dequeue, n }, _from, state) do
case Redix.command(state.redis, ["EVALSHA", @mrpop_lpush_src_dest_script_sha, 2, "queue:#{state.queue_name}",
inprogress(state.queue_name, state.node_id), min(@max_jobs, n)]) do
{ :ok, [] } ->
{ :reply, [], state }
{ :ok, jobs } ->
{ :reply, jobs, state }
{ :error, %Redix.Error{message: message} } ->
Logger.error("Failed to fetch jobs: #{message}")
{ :stop, :redis_failed, :redis_failed, state }
{ :error, _ } ->
{ :reply, :redis_failed, state }
end
end
def handle_call({ :retry, job, failed_at, exception, stacktrace }, _from, state) do
retry_count = (job.retry_count || 0) + 1
job = build_retry_job(job, retry_count, failed_at, exception, stacktrace)
if retry_count <= @max_retry do
case RetrySet.add(job, failed_at, state.redis) do
:ok -> :ok
error -> Logger.error("Failed to add job_id #{job.jid} to the retry set. Error: #{inspect error}")
end
else
Logger.info("Max retries reached to job_id #{job.jid}, job: #{inspect job}")
case DeadSet.add(job, failed_at, state.redis) do
{ :ok, _ } -> :ok
error -> Logger.error("Failed to add job_id #{job.jid} to the dead set. Error: #{inspect error}")
end
end
{ :reply, :ok, state }
end
defp build_retry_job(job, retry_count, failed_at, exception, stacktrace) do
job = %{ job | error_backtrace: Exception.format_stacktrace(stacktrace),
error_message: Exception.message(exception),
retry_count: retry_count }
if retry_count > 1 do
# Set the retried_at if this job was already retried at least once
%{ job | retried_at: failed_at }
else
# Set the failed_at if this the first time the job failed
%{ job | failed_at: failed_at }
end
end
@doc false
def handle_cast({:ack, job}, state) do
case Redix.command(state.redis, ["LREM", inprogress(state.queue_name, state.node_id), "-1", job.original_json]) do
{ :ok, 1 } -> :ok
_ -> Logger.error("Failed to acknowledge job #{inspect job}")
end
{ :noreply, state }
end
defp inprogress(queue_name, node_id) do
"inprogress:#{queue_name}:#{node_id}"
end
end