Current section
Files
Jump to
Current section
Files
lib/queuetopia/scheduler.ex
defmodule Queuetopia.Scheduler do
@moduledoc """
The Queuetopia scheduler, polling with a defined interval the next available queues.
"""
use GenServer
alias Queuetopia.Queue
alias Queuetopia.Queue.Job
@type option :: {:poll_interval, pos_integer()}
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: opts[:name])
end
def send_poll(scheduler_pid) when is_pid(scheduler_pid) do
unless has_poll_messages?(scheduler_pid) do
Process.send(scheduler_pid, {:poll, one_time?: true}, [])
end
end
defp has_poll_messages?(scheduler_pid) do
{:messages, messages} = Process.info(scheduler_pid, :messages)
Enum.any?(messages, &match?({:poll, _}, &1))
end
@impl true
@spec init([option]) :: {:ok, map}
def init(opts) do
Process.send(self(), {:poll, one_time?: false}, [])
state = %{
repo: Keyword.get(opts, :repo),
task_supervisor_name: Keyword.get(opts, :task_supervisor_name),
poll_interval: Keyword.get(opts, :poll_interval),
scope: Keyword.get(opts, :scope),
jobs: %{}
}
{:ok, state}
end
@impl true
def handle_info({:poll, one_time?: one_time?}, state) do
%{
task_supervisor_name: task_supervisor_name,
poll_interval: poll_interval,
repo: repo,
scope: scope,
jobs: jobs
} = state
jobs = poll_queues(task_supervisor_name, poll_interval, repo, scope, jobs, one_time?)
{:noreply, %{state | jobs: jobs}}
end
def handle_info(
{:DOWN, ref, :process, _pid, _reason},
%{jobs: jobs, repo: repo, scope: scope} = state
) do
job = Map.get(jobs, ref)
:ok = handle_task_result(repo, job, {:error, "down"})
Queue.unlock_queue(repo, scope, job.queue)
{:noreply, %{state | jobs: Map.delete(jobs, ref)}}
end
def handle_info({:kill, task}, %{jobs: jobs, repo: repo} = state) do
Task.shutdown(task, :brutal_kill)
job = Map.get(jobs, task.ref)
:ok = handle_task_result(repo, job, {:error, "timeout"})
{:noreply, %{state | jobs: Map.delete(jobs, task.ref)}}
end
def handle_info({ref, task_result}, %{jobs: jobs, repo: repo, scope: scope} = state) do
Process.demonitor(ref, [:flush])
job = Map.get(jobs, ref)
:ok = handle_task_result(repo, job, task_result)
Queue.unlock_queue(repo, scope, job.queue)
send_poll(self())
{:noreply, %{state | jobs: Map.delete(jobs, ref)}}
end
defp handle_task_result(repo, job, result) do
unless is_nil(job) do
Queue.persist_result(repo, job, result)
end
:ok
end
defp poll_queues(task_supervisor_name, poll_interval, repo, scope, jobs, one_time?) do
Queue.release_expired_locks(repo, scope)
jobs =
Queue.list_available_pending_queues(repo, scope)
|> Enum.map(&perform_next_pending_job(&1, task_supervisor_name, repo, scope))
|> Enum.reject(&is_nil(&1))
|> Enum.into(%{})
|> Map.merge(jobs)
unless one_time? do
Process.send_after(self(), {:poll, one_time?: false}, poll_interval)
end
jobs
end
defp perform_next_pending_job(
queue,
task_supervisor_name,
repo,
scope
) do
with %Job{} <- job = Queue.get_next_pending_job(repo, scope, queue),
{:ok, job} <- Queue.fetch_job(repo, job) do
task = Task.Supervisor.async_nolink(task_supervisor_name, Queue, :perform, [job])
Process.send_after(self(), {:kill, task}, job.timeout)
{task.ref, job}
else
_ -> nil
end
end
end