defmodule Runbox.Scenario.Helper do
@moduledoc """
Support for helper processes for scenarios.
Provides a supervisor and process registry under which scenarios can start
their side-kick processes intended for housekeeping tasks.
"""
use Supervisor
@worker_sup Runbox.Scenario.Helper.WorkerSup
@registry Runbox.Scenario.Helper.Registry
@doc """
Starts a supervisor with dynamic supervisor and process registry.
"""
@spec start_link(any()) :: Supervisor.on_start()
def start_link(_) do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end
@doc """
Executes a given `callback` over running helper.
Checks whether the helper with `name` registered under helper registry is
already running. If not, then it starts the process according to `child_spec`
under the dynamic supervisor. In either case, it passes the helper process
pid to the `callback`.
It is the responsibility of the helper process to register its name with the
helper registry via the `via_registry/1`.
## Examples
iex> child_spec = %{
...> id: MyHelper,
...> start: {
...> Agent,
...> :start_link,
...> [fn -> 1 end, [name: Helper.via_registry(MyHelper)]]
...> }
...> }
iex> :ok = Helper.with_helper(MyHelper, child_spec, fn pid -> Agent.update(pid, & &1 + 1) end)
iex> Helper.with_helper(MyHelper, child_spec, fn pid -> Agent.get(pid, & &1) end)
2
"""
@spec with_helper(any(), child_spec, (pid() -> any())) :: any()
when child_spec: :supervisor.child_spec() | {module(), term()} | module()
def with_helper(name, child_spec, callback) do
with {:ok, pid} <- ensure_started(name, child_spec) do
callback.(pid)
end
end
@doc """
Returns a `:via` name to be used in the helper process registry.
"""
@spec via_registry(term()) :: {:via, Registry, term()}
def via_registry(name), do: {:via, Registry, {@registry, name}}
@impl true
def init(_) do
children = [
{DynamicSupervisor, strategy: :one_for_one, name: @worker_sup},
{Registry, keys: :unique, name: @registry}
]
Supervisor.init(children, strategy: :one_for_all)
end
defp ensure_started(name, child_spec) do
case Registry.lookup(@registry, name) do
[{pid, _}] -> {:ok, pid}
[] -> start_worker(child_spec)
end
end
defp start_worker(child_spec) do
case DynamicSupervisor.start_child(@worker_sup, child_spec) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
{:error, _} = error -> error
end
end
end