Worker hooks are called after a job finishes executing. They can be defined as callback functions on the worker, or in a separate module for reuse across workers.
Hooks are called synchronously, from within the job's process with safety applied. Any exceptions or crashes are caught and logged, they won't cause the job to fail or the queue to crash.
Hooks do not modify the job or execution results. Think of them as a convenient alternative to globally attached telemetry handlers. They are purely for side-effects such as cleanup, logging, recording metrics, broadcasting notifications, updating other records, error notifications, etc.
There are three mechanisms for defining and attaching an c:after_process/2
hook:
- Implicitly—hooks are defined directly on the worker and they only run for that worker
- Explicitly—hooks are listed when defining a worker and they run anywhere they are listed
- Globally—hooks are executed for all Pro workers
It's possible to combine each type of hook on a single worker. When multiple hooks are stacked they're executed in the order: implicit, explicit, and then global.
An c:after_process/2
hook is called with the job and an execution state corresponding to the
result from process/1
:
complete
—whenprocess/1
returns:ok
or{:ok, result}
cancel
—whenprocess/1
returns{:cancel, reason}
discard
—when a job errors and exhausts retries, or returns{:discard, reason}
error
—when a job crashes, raises an exception, or returns{:error, value}
snooze
—when a job returns{:snooze, seconds}
First, here's how to define a single implicit local hook on the worker using
c:after_process/2
:
defmodule MyApp.HookWorker do
use Oban.Pro.Worker
@impl Oban.Pro.Worker
def process(_job) do
# ...
end
@impl Oban.Pro.Worker
def after_process(state, %Job{} = job) do
MyApp.Notifier.broadcast("oban-jobs", {state, %{id: job.id}})
end
end
Any module that exports c:after_process/2
can be used as a hook. For example, here we'll
define a shared error notification hook:
defmodule MyApp.ErrorHook do
def after_process(state, job) when state in [:discard, :error] do
error = job.unsaved_error
extra = Map.take([:attempt, :id, :args, :max_attempts, :meta, :queue, :worker])
Sentry.capture_exception(error.reason, stacktrace: error.stacktrace, extra: extra)
end
def after_process(_state, _job), do: :ok
end
defmodule MyApp.HookWorker do
use Oban.Pro.Worker, hooks: [MyApp.ErrorHook]
@impl Oban.Pro.Worker
def process(_job) do
# ...
end
end
The same module can be attached globally, for all Oban.Pro.Worker
modules, using
attach_hook/1
:
:ok = Oban.Pro.Worker.attach_hook(MyApp.ErrorHook)