defmodule Aprsme.BroadcastTaskSupervisor do @moduledoc """ A dedicated Task.Supervisor for handling broadcast operations efficiently. This supervisor manages a pool of tasks for broadcasting packets to multiple clients concurrently, preventing the main GenServer processes from blocking on I/O operations. """ use Supervisor @pool_name :broadcast_task_pool def start_link(init_arg) do Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) end @impl true def init(_init_arg) do # Calculate optimal pool size based on schedulers pool_size = System.schedulers_online() * 2 children = [ # Create a Task.Supervisor with a specific name {Task.Supervisor, name: @pool_name} ] # Store pool configuration in persistent_term for fast access :persistent_term.put({__MODULE__, :pool_size}, pool_size) :persistent_term.put({__MODULE__, :pool_name}, @pool_name) Supervisor.init(children, strategy: :one_for_one) end @doc """ Broadcasts a message to multiple topics asynchronously using the task pool. Uses `Task.Supervisor.start_child/2` for fire-and-forget execution. The spawned task is not linked to the caller and does not send reply messages back, avoiding orphaned `{ref, result}` and `:DOWN` messages in the calling process mailbox. ## Parameters - topics: List of PubSub topics to broadcast to - message: The message to broadcast - pubsub: The PubSub server (defaults to Aprsme.PubSub) ## Returns - {:ok, pid} of the spawned task """ def broadcast_async(topics, message, pubsub \\ Aprsme.PubSub) do Task.Supervisor.start_child(@pool_name, fn -> # Use Stream for memory efficiency with large topic lists topics # Process in chunks to balance load |> Stream.chunk_every(10) |> Enum.each(fn topic_chunk -> broadcast_to_topics(topic_chunk, message, pubsub) end) end) end @doc """ Broadcasts a single message to a topic asynchronously. """ def broadcast_one_async(topic, message, pubsub \\ Aprsme.PubSub) do Task.Supervisor.start_child(@pool_name, fn -> Phoenix.PubSub.broadcast(pubsub, topic, message) end) end @doc """ Executes a function asynchronously in the broadcast pool. This is useful for any broadcast-related async operation. """ def async_execute(fun) when is_function(fun, 0) do Task.Supervisor.start_child(@pool_name, fun) end @doc """ Gets statistics about the broadcast pool. """ def get_stats do children = Task.Supervisor.children(@pool_name) %{ active_tasks: length(children), pool_size: :persistent_term.get({__MODULE__, :pool_size}, 0), scheduler_usage: scheduler_usage() } end # Calculate approximate scheduler usage defp scheduler_usage do # In test env, skip expensive sampling and return dummy value if Application.get_env(:aprsme, :env) == :test do 0.0 else sample_seconds = Application.get_env(:aprsme, :scheduler_sample_seconds, 1) compute_scheduler_usage(sample_seconds) end rescue _ -> 0.0 end # When sample_seconds is 0, use sample-pair API to avoid blocking. defp compute_scheduler_usage(0) do sample = :scheduler.sample_all() sample |> :scheduler.utilization() |> Enum.map(fn {_, usage, _} -> usage end) |> Enum.sum() |> Kernel./(System.schedulers_online()) |> Float.round(2) end defp compute_scheduler_usage(seconds) when is_integer(seconds) and seconds > 0 do seconds |> :scheduler.utilization() |> Enum.map(fn {_, usage, _} -> usage end) |> Enum.sum() |> Kernel./(System.schedulers_online()) |> Float.round(2) end # Private helper to broadcast to multiple topics defp broadcast_to_topics(topics, message, pubsub) do Enum.each(topics, fn topic -> Phoenix.PubSub.broadcast(pubsub, topic, message) end) end end