prop/vendor/oban_pro/lib/oban/pro/diagnostics.ex
Graham McIntire e99bf06eb4
deps: re-vendor oban_pro 1.7.0 (revert hex-repo dep)
Previous commit (3c988a5f) switched oban_pro to the licensed hex
repo at deploy time. Reverting that — keep all three Pro packages
vendored so prod image builds don't depend on oban.pro reachability
or auth on every CI run.

oban_pro 1.7.0 dropped into vendor/oban_pro via
`mix hex.package fetch oban_pro 1.7.0 --repo=oban --unpack`. mix.exs
goes back to `path: "vendor/oban_pro"` (matching oban_met / oban_web,
which were already vendored — and stay vendored since the licensed
repo only has older versions of those: 0.1.11 / 2.10.6 vs the
1.1.0 / 2.12.1 we vendor).

Schema migration from 3c988a5f stays — 1.7.0 tables/indexes are
already applied. No code changes.
2026-04-30 09:29:55 -05:00

135 lines
2.9 KiB
Elixir

defmodule Oban.Pro.Diagnostics do
@moduledoc false
use GenServer
alias __MODULE__, as: State
alias Oban.Notifier
defstruct [:timer, interval: :timer.seconds(5), subscribed: MapSet.new()]
@info_keys ~w(
current_stacktrace
heap_size
memory
message_queue_len
reductions
stack_size
status
)a
@doc false
def start_link(opts \\ []) do
state = struct!(State, opts)
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
# Callbacks
@impl GenServer
def init(state) do
Process.flag(:trap_exit, true)
{:ok, schedule_discover(state)}
end
@impl GenServer
def terminate(_reason, state) do
if is_reference(state.timer), do: Process.cancel_timer(state.timer)
:ok
end
@impl GenServer
def handle_call(:discover, _from, state) do
{:reply, :ok, discover_and_subscribe(state)}
end
@impl GenServer
def handle_info(:discover, state) do
{:noreply, discover_and_subscribe(state)}
end
def handle_info({:notification, :diagnostics, %{"job_id" => job_id}}, state) do
finder = fn conf ->
case Registry.lookup(Oban.Registry, {conf.name, {:executing, job_id}}) do
[{pid, _}] -> {conf, pid}
_ -> nil
end
end
case Enum.find_value(list_oban_instances(), finder) do
{conf, pid} ->
payload = %{"info" => process_info(pid), "job_id" => job_id, "node" => inspect(conf.node)}
Notifier.notify(conf, :diagnostics_reply, payload)
nil ->
:ok
end
{:noreply, state}
end
def handle_info(_message, state) do
{:noreply, state}
end
# Helpers
defp discover_and_subscribe(state) do
state =
Enum.reduce(list_oban_instances(), state, fn conf, acc ->
if MapSet.member?(acc.subscribed, conf.name) do
acc
else
:ok = Notifier.listen(conf.name, [:diagnostics])
%{acc | subscribed: MapSet.put(acc.subscribed, conf.name)}
end
end)
state
|> cancel_timer()
|> schedule_discover()
end
defp list_oban_instances do
match = [{{{:"$1", Oban.Notifier}, :_, :_}, [], [:"$1"]}]
Oban.Registry
|> Registry.select(match)
|> Enum.map(&Oban.config/1)
end
defp process_info(pid) do
case Process.info(pid, @info_keys) do
nil ->
%{}
info ->
info
|> Keyword.update!(:current_stacktrace, &format_stacktrace/1)
|> Keyword.update!(:status, &to_string/1)
|> Map.new(fn {key, val} -> {to_string(key), val} end)
end
end
defp format_stacktrace(nil), do: ""
defp format_stacktrace(stacktrace), do: Exception.format_stacktrace(stacktrace)
defp cancel_timer(%{timer: timer} = state) when is_reference(timer) do
Process.cancel_timer(timer)
%{state | timer: nil}
end
defp cancel_timer(state), do: state
defp schedule_discover(state) do
timer = Process.send_after(self(), :discover, state.interval)
%{state | timer: timer}
end
end