defmodule CouncilEx.Supervisor do
@moduledoc """
Optional `DynamicSupervisor` wrapper for grouping council runs.
CouncilEx itself does not ship a bundled supervisor — `CouncilEx.start/3`
spawns runners unsupervised (`GenServer.start/3` semantics) and
`CouncilEx.start_link/3` links them to the caller. Use this module
only when you want **group management** of multiple runs:
* Tenant isolation — one supervisor per tenant; bulk-cancel via
`Supervisor.stop/1` or `terminate_all/1`.
* Visibility — `which_children/1` enumerates in-flight runs.
* Lifecycle ownership — runs survive caller exit, die when the
supervisor exits.
Children are `:temporary` (no restart on crash) — re-running a
partially-completed council from scratch wastes tokens and produces
weird state.
## Setup
Place an instance in your supervision tree:
children = [
{CouncilEx.Supervisor, name: MyApp.RunSup}
]
Then start runs against it:
{:ok, pid} =
CouncilEx.Supervisor.start_link(MyApp.RunSup, council, input)
# Fetch the run_id when needed (e.g. for PubSub subscribe across
# processes, persistence as a durable handle):
run_id = CouncilEx.RunServer.run_id(pid)
Pass any `CouncilEx.start/3` opt (e.g. `:registry`, `:recorder`,
`:verbose`) — they're forwarded to the runner.
"""
use DynamicSupervisor
alias CouncilEx.RunServer
@doc "Start the supervisor. Pass `:name` (required for named lookup)."
@spec start_link(keyword()) :: Supervisor.on_start()
def start_link(opts) do
{init_opts, sup_opts} = Keyword.split(opts, [:name])
DynamicSupervisor.start_link(__MODULE__, sup_opts, init_opts)
end
@impl true
def init(_opts), do: DynamicSupervisor.init(strategy: :one_for_one)
@doc """
Start a run as a child of this supervisor. The runner is linked to
the supervisor (not the caller).
Returns `{:ok, pid}`. Fetch the run_id from the pid via
`CouncilEx.RunServer.run_id/1` if needed, or supply your own with
the `:run_id` opt. Validation, options, and PubSub semantics match
`CouncilEx.start/3`.
"""
@spec start_link(
Supervisor.supervisor(),
module() | CouncilEx.DynamicCouncil.t(),
term(),
keyword()
) :: {:ok, pid()} | {:error, term()}
def start_link(supervisor, council, input, opts \\ []) do
case prepare(council, input, opts) do
{:ok, council_id, spec, server_opts} ->
spawn(supervisor, council_id, spec, input, server_opts)
{:error, _} = err ->
err
end
end
@doc """
Terminate every child runner under `supervisor`. Synchronous.
Each runner receives `:shutdown`. Subscribers monitoring a runner pid
see `:DOWN`. Use this for tenant teardown.
"""
@spec terminate_all(Supervisor.supervisor()) :: :ok
def terminate_all(supervisor) do
for {_, pid, _, _} <- DynamicSupervisor.which_children(supervisor),
is_pid(pid) do
DynamicSupervisor.terminate_child(supervisor, pid)
end
:ok
end
defp prepare(%CouncilEx.DynamicCouncil{} = council, _input, opts) do
case CouncilEx.DynamicCouncil.validate(council) do
:ok ->
spec = CouncilEx.DynamicCouncil.to_spec(council)
{:ok, "dynamic:" <> council.id, spec, opts}
{:error, errors} ->
{:error, {:invalid_council, errors}}
end
end
defp prepare(council_module, _input, opts) when is_atom(council_module) do
case CouncilEx.validate(council_module) do
:ok ->
spec = council_module.__council__()
{:ok, council_module, spec, opts}
{:error, errors} ->
{:error, {:invalid_council, errors}}
end
end
defp spawn(supervisor, council_id, spec, input, opts) do
run_id = Keyword.get(opts, :run_id) || generate_run_id()
registry = Keyword.get(opts, :registry, RunServer.default_registry())
callers = [self() | Process.get(:"$callers", [])]
child_spec =
Supervisor.child_spec(
{RunServer,
[
run_id: run_id,
council: council_id,
spec: spec,
input: input,
user_opts: opts,
registry: registry,
callers: callers
]},
restart: :temporary
)
case DynamicSupervisor.start_child(supervisor, child_spec) do
{:ok, pid} -> {:ok, pid}
{:error, _} = err -> err
end
end
defp generate_run_id do
"run_" <> (:crypto.strong_rand_bytes(8) |> Base.url_encode64(padding: false))
end
end