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