James_E
DynamicSupervisor: how to start child iff it doesn't exist?
I’ve got this bit of code; I’m wondering why create_or_open_room always creates a new room, even when there is an existing child process with the same ID:
If a child specification with the specified ID already exists,
child_specis discarded and this function returns an error with:already_startedor:already_presentif the corresponding child process is running or not, respectively.
Supervisor.start_child/2 — Elixir v1.17.3
defmodule FooApp.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
require Logger
def create_or_open_room(<<room_name::binary>>) do # FIXME this is supposed to be idempotent
case DynamicSupervisor.start_child(
FooApp.RoomSupervisor,
%{id: room_name, start: {GenServer, :start_link, [FooApp.Application.Model, room_name]}}
) do
{:ok, pid} -> Logger.debug("=====STARTED #{inspect pid}====="); pid
:ignore -> raise "unreachable"
{:error, {:already_started, pid}} -> Logger.debug("=====IDEMPOTENCE OK #{inspect pid}====="); pid
{:error, error} -> raise inspect error
end
end
@impl true
def start(_type, _args) do
children = [
FooAppWeb.Telemetry,
FooApp.Repo,
{Ecto.Migrator,
repos: Application.fetch_env!(:fooApp, :ecto_repos),
skip: skip_migrations?()},
# {DNSCluster, query: Application.get_env(:fooApp, :dns_cluster_query) || :ignore},
# {Phoenix.PubSub, name: FooApp.PubSub}, # for general app communication
{Phoenix.PubSub, name: FooApp.RoomPubSub}, # ONLY for room statechange announcements, to avoid collision between UGC room names and an actual system topic
# Start the Finch HTTP client for sending emails
# {Finch, name: FooApp.Finch},
# Start a worker by calling: FooApp.Worker.start_link(arg)
# {FooApp.Worker, arg},
{DynamicSupervisor, name: FooApp.RoomSupervisor},
# Start to serve requests, typically the last entry
FooAppWeb.Endpoint
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: FooApp.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
FooAppWeb.Endpoint.config_change(changed, removed)
:ok
end
defp skip_migrations?() do
# By default, sqlite migrations are run when using a release
System.get_env("RELEASE_NAME") != nil
end
end
FooApp.Application.Model (a boring GenServer, not relevant, including for completeness)
defmodule FooApp.Application.Model do
use GenServer
require Logger
defmodule State do
@enforce_keys [:name, :resources]
defstruct [:name, :resources, :_pubsub_topic]
end
defmodule Resource do
@enforce_keys [:name]
defstruct [:name, status: "undefined"]
@allowable_statuses ["green", "green-with-exception", "red"]
def status_ok(status) do
status in @allowable_statuses
end
end
defp list_update_such(list, fun_pred, fun) do
# TODO consider overhauling/replacing this with a new data structure entirely
# https://forum.elixirforum.net/t/need-to-display-resources-in-the-exact-order-they-are-declared-ordered-map/67829/4?u=james_e
Enum.map(list, &if fun_pred.(&1) do fun.(&1) else &1 end)
end
@impl true
def init(name) do
resource_names = [ # FIXME actually load this from ecto
"FROG-0",
"FROG-1",
"FROG-2",
"FROG-3",
"FROG-4",
"FROG-5",
"FROG-6",
"FROG-7",
];
{:ok, %State{
name: name,
resources: resource_names |> Enum.map(&%Resource{name: &1}),
_pubsub_topic: name
}}
end
@impl true
def handle_continue(:broadcast_statechange, state) do
# TODO this seems located inappropriately far from the call to subscribe; is there a better pattern for this?
Phoenix.PubSub.broadcast!(FooApp.RoomPubSub, state._pubsub_topic, {:statechange, state});
{:noreply, state}
end
@impl true
def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call({:act, actor, action}, from, state) do
case action do
{:set_resource_status, resource_name, new_status} -> (
with \
true <- actor === "director" || {:error, "Unauthorized"},
resources = state.resources,
true <- Resource.status_ok(new_status) || {:error, "Invalid status"}
do
{:reply, :ok, %{state |
resources: list_update_such(resources, &(&1.name === resource_name), &%{&1 | status: new_status})
}, {:continue, :broadcast_statechange}}
else
{:error, error} -> {:reply, {:error, error}, state}
end
)
_ -> (
Logger.warning("Client #{inspect from} attempted invalid action");
{:reply, {:error, "Action incongruent with current state"}, state}
)
end
end
end
Marked As Solved
James_E
Can’t edit the post because it’s too old; here’s an implementation for posterity with proper documentation
@doc """
Exactly like [DynamicSupervisor.start_child/2](https://hexdocs.pm/elixir/1.17/DynamicSupervisor.html#start_child/2)
except that the "id" field is not [disregarded](https://hexdocs.pm/elixir/1.17/Supervisor.html#module-child-specification).
Only works with simple GenServer supervisees for now.
## Usage
start_child_idempotent(
Foo.Application.DynamicSupervisorForModelInstances, %{
id: locally_unique_id_for_this_model_instance, start: {GenServer, :start_link, [
Foo.Application.Model,
model_arg
]}})
"""
@spec start_child_idempotent(Supervisor.supervisor(), Supervisor.child_spec()) :: DynamicSupervisor.on_start_child()
def start_child_idempotent(dynamic_supervisor, child_spec = %{id: id, start: _}) do
# https://hexdocs.pm/elixir/1.17/GenServer.html#module-name-registration
# FIXME: https://hexdocs.pm/elixir/1.17/Registry.html#module-using-in-via
reg_name = {:global, {
"#{__MODULE__}.start_child_idempotent",
GenServer.whereis(dynamic_supervisor) || dynamic_supervisor,
id
}}
# https://hexdocs.pm/elixir/1.15/DynamicSupervisor.html#start_child/2:~:text=while%20the%20:id%20field%20is%20still%20required,the%20value%20is%20ignored
child_spec = Map.update!(child_spec, :start, fn
{m = GenServer, f = :start_link, [m1, x]} -> {m, f, [m1, x, [name: reg_name]]}
{m = GenServer, f = :start_link, [m1, x, opts]} -> {m, f, [m1, x, opts ++ [name: reg_name]]}
{m, f = :start_link, [x, opts]} when is_list(opts) -> {m, f, [x, opts ++ [name: reg_name]]}
start -> raise %ArgumentError{message: "only GenServer style start_link children supported at this time.\n#{inspect start}"}
end)
# https://github.com/elixir-lang/elixir/blob/v1.12.2/lib/elixir/lib/gen_server.ex#L929
# https://github.com/erlang/otp/blob/OTP-24.2.1/lib/stdlib/src/gen.erl#L83
case DynamicSupervisor.start_child(dynamic_supervisor, child_spec) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
other -> other
end
end
Also Liked
al2o3cr
The docs for DynamicSupervisor.start_child mention that the id is required but ignored:
Note that while the
:idfield is still required in the spec, the value is ignored and therefore does not need to be unique.
dimitarvp
Come on now, spec is not “wack”, you just need a name if you want to, you know, have a named process. ![]()
James_E
I was referring the @spec — where the DynamicSupervisor’s child’s id field is required, yet ignored — that was a red herring that confused me when trying to implement this.
The ID here isn’t meant to be a global name, only a local ID within the scope of that specific DynamicSupervisor.







