Packages
verk
0.9.13
1.7.3
1.7.2
1.7.1
1.7.0
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.0
1.4.1
1.4.0
1.3.0
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.2
1.0.1
1.0.0
0.14.1
0.14.0
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
Verk is a job processing system backed by Redis.
Current section
Files
Jump to
Current section
Files
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