Current section
Files
Jump to
Current section
Files
lib/broadway_dashboard/metrics.ex
defmodule BroadwayDashboard.Metrics do
@moduledoc false
use GenServer
alias BroadwayDashboard.Counters
alias BroadwayDashboard.Telemetry
alias BroadwayDashboard.Teleporter
@default_interval 1_000
# It does polling every 1 second for new metrics from
# pipelines to pages listening for events.
# It monitor pages and unsubscribe them in case of exit.
def listen(target_node, parent, pipeline) do
name = server_name(pipeline)
with :ok <- check_pipeline_running_at_node(pipeline, target_node),
{:ok, server_name} <- ensure_server_started_at_node(pipeline, name, target_node) do
GenServer.call(server_name, {:listen, parent})
end
end
def server_name(pipeline) when is_atom(pipeline) do
:"broadway_dashboard_#{inspect(pipeline)}"
end
def server_name({:via, registry, term}) when is_atom(registry) do
:"broadway_dashboard_#{inspect(registry)}_#{inspect(term)}"
end
defp check_pipeline_running_at_node(pipeline, target_node) do
result =
if target_node == node() do
GenServer.whereis(pipeline)
else
:rpc.call(target_node, GenServer, :whereis, [pipeline])
end
case result do
pid when is_pid(pid) ->
:ok
_ ->
{:error, :pipeline_not_found}
end
end
defp ensure_server_started_at_node(pipeline, name, target_node) when target_node == node() do
if GenServer.whereis(name) do
{:ok, name}
else
with {:ok, _pid} <- start(pipeline: pipeline, name: name) do
{:ok, name}
end
end
end
defp ensure_server_started_at_node(pipeline, name, target_node) do
case :rpc.call(target_node, GenServer, :whereis, [name]) do
pid when is_pid(pid) ->
{:ok, {name, target_node}}
nil ->
with :ok <- Teleporter.teleport_metrics_code(target_node),
{:ok, _pid} <-
:rpc.call(target_node, __MODULE__, :start, [
[pipeline: pipeline, name: name]
]) do
{:ok, {name, target_node}}
else
{:error, {:already_started, _pid}} ->
{:ok, {name, target_node}}
_error ->
{:error, :not_able_to_start_remotely}
end
{:badrpc, _} = error ->
{:error, error}
end
end
def ensure_counters_restarted(pipeline) do
name = server_name(pipeline)
GenServer.call(name, :ensure_counters_restarted)
end
def start(opts) do
GenServer.start(__MODULE__, opts, name: Keyword.fetch!(opts, :name))
end
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: Keyword.fetch!(opts, :name))
end
@impl true
def init(opts) do
Process.flag(:trap_exit, true)
interval = opts[:interval] || @default_interval
pipeline = opts[:pipeline]
topology = Broadway.topology(pipeline)
counters = Counters.build(topology)
state = %{
pipeline: pipeline,
listeners: Map.new(),
interval: interval,
counters: counters,
shutdown_timer: nil,
mode: opts[:refresh_mode] || :auto
}
Telemetry.attach(self(), pipeline, counters)
{:ok, Map.put(state, :timer, maybe_schedule_refresh(state))}
end
defp maybe_schedule_refresh(state) do
unless state.mode == :manual do
Process.send_after(self(), :refresh, state.interval)
end
end
@impl true
def handle_call(:ensure_counters_restarted, _, state) do
topology = Broadway.topology(state.pipeline)
counters = Counters.build(topology)
Telemetry.detach(self())
Telemetry.attach(self(), state.pipeline, counters)
{:reply, :ok, %{state | counters: counters}}
end
@impl true
def handle_call({:listen, parent}, _, state) do
ref = Process.monitor(parent)
listeners = Map.put(state.listeners, ref, parent)
if state.shutdown_timer, do: Process.cancel_timer(state.shutdown_timer)
{:reply, {:ok, build_update_payload(state)},
%{state | listeners: listeners, shutdown_timer: nil}}
end
@impl true
def handle_info(:refresh, state) do
payload = build_update_payload(state)
for pid <- Map.values(state.listeners),
do: send(pid, {:update_pipeline, payload})
{:noreply, %{state | timer: maybe_schedule_refresh(state)}}
end
@impl true
def handle_info({:DOWN, ref, _, _pid, _}, state) do
listeners = Map.delete(state.listeners, ref)
shutdown_timer =
if listeners == %{} do
maybe_schedule_shutdown(state)
end
{:noreply, %{state | listeners: listeners, shutdown_timer: shutdown_timer}}
end
@impl true
def handle_info(:shutdown, state) do
{:stop, :normal, state}
end
@impl true
def handle_info(_, state) do
{:noreply, state}
end
@impl true
def terminate(_reason, _state) do
Telemetry.detach(self())
:ok
end
defp build_update_payload(state) do
topology = Broadway.topology(state.pipeline)
topology_workload = Counters.topology_workload(state.counters, topology)
{:ok, {successful, failed}} = Counters.count(state.counters)
%{
pipeline: state.pipeline,
topology_workload: topology_workload,
successful: successful,
failed: failed
}
end
defp maybe_schedule_shutdown(state) do
unless state.mode == :manual or state.shutdown_timer do
Process.send_after(self(), :shutdown, state.interval * 5)
end
end
end