Current section

Files

Jump to
oban lib oban queue executor.ex
Raw

lib/oban/queue/executor.ex

defmodule Oban.Queue.Executor do
@moduledoc false
require Logger
alias Oban.{Breaker, Config, Job, Query, Worker}
@type success :: {:success, Job.t()}
@type failure :: {:failure, Job.t(), Worker.t(), atom(), term()}
@spec call(Job.t(), Config.t()) :: :success | :failure
def call(%Job{} = job, %Config{} = conf) do
start_time = System.system_time(:microsecond)
start_mono = System.monotonic_time(:microsecond)
:telemetry.execute([:oban, :started], %{start_time: start_time}, job_meta(job))
case safe_call(job) do
{:success, ^job} ->
Breaker.with_retry(fn -> report_success(conf, job, start_mono) end)
{:failure, ^job, kind, error, stack} ->
Breaker.with_retry(fn -> report_failure(conf, job, start_mono, kind, error, stack) end)
end
end
@spec safe_call(Job.t()) :: success() | failure()
def safe_call(%Job{} = job) do
case Job.worker_module(job) do
{:ok, worker} ->
case worker.timeout(job) do
:infinity ->
perform_inline(worker, job)
timeout when is_integer(timeout) ->
perform_timed(worker, job, timeout)
end
{:error, error, stacktrace} ->
{:failure, job, :error, error, stacktrace}
end
end
@spec perform_inline(Worker.t(), Job.t()) :: success() | failure()
def perform_inline(worker, %Job{args: args} = job) do
case worker.perform(args, job) do
:ok ->
{:success, job}
{:ok, _result} ->
{:success, job}
{:error, error} ->
{:failure, job, :error, error, current_stacktrace()}
returned ->
Logger.warn(fn ->
"""
Expected #{worker}.perform/2 to return :ok, `{:ok, value}`, or {:error, reason}. Instead
received:
#{inspect(returned, pretty: true)}
The job will be considered a success.
"""
end)
{:success, job}
end
rescue
exception ->
{:failure, job, :exception, exception, __STACKTRACE__}
catch
kind, value ->
{:failure, job, kind, value, __STACKTRACE__}
end
@spec perform_timed(Worker.t(), Job.t(), timeout()) :: success() | failure()
def perform_timed(worker, job, timeout) do
task = Task.async(fn -> perform_inline(worker, job) end)
case Task.yield(task, timeout) || Task.shutdown(task) do
{:ok, reply} ->
reply
nil ->
{:failure, job, :error, :timeout, current_stacktrace()}
end
end
@spec report_success(Config.t(), Job.t(), integer()) :: :success
def report_success(conf, job, start_mono) do
Query.complete_job(conf, job)
:telemetry.execute([:oban, :success], %{duration: duration(start_mono)}, job_meta(job))
:success
end
@spec report_failure(Config.t(), Job.t(), integer(), atom(), any(), list()) :: :failure
def report_failure(conf, job, start_mono, kind, error, stack) do
Query.retry_job(conf, job, worker_backoff(job), kind, error, stack)
meta =
job
|> job_meta()
|> Map.merge(%{kind: kind, error: error, stack: stack})
:telemetry.execute([:oban, :failure], %{duration: duration(start_mono)}, meta)
:failure
end
# Helpers
defp current_stacktrace do
self()
|> Process.info(:current_stacktrace)
|> elem(1)
end
defp duration(start_mono), do: System.monotonic_time(:microsecond) - start_mono
defp job_meta(job), do: Map.take(job, [:id, :args, :queue, :worker, :attempt, :max_attempts])
defp worker_backoff(%Job{attempt: attempt} = job) do
case Job.worker_module(job) do
{:ok, worker} -> worker.backoff(attempt)
_ -> Worker.default_backoff(attempt)
end
end
end