Skip to content

Instantly share code, notes, and snippets.

@James-E-A
Last active February 10, 2025 17:50
Show Gist options
  • Save James-E-A/7a8393603870c9d3a2f1903bc8dead82 to your computer and use it in GitHub Desktop.
Save James-E-A/7a8393603870c9d3a2f1903bc8dead82 to your computer and use it in GitHub Desktop.
Elixir DynamicSupervisor start child if it doesn't already exist
defmodule FooApp.Util do
@doc """
Exactly like [DynamicSupervisor.start_child/2](https://hexdocs.pm/elixir/1.18/DynamicSupervisor.html#start_child/2)
except that the "id" field is not [disregarded](https://hexdocs.pm/elixir/1.15/DynamicSupervisor.html#start_child/2:~:text=while%20the%20:id%20field%20is%20still%20required,the%20value%20is%20ignored),
but instead used as a unique key.
Only works with simple GenServer supervisees for now.
Does NOT work with remote DynamicSupervor or distributed systems for now.
## Example
iex> defmodule FooApp.Model do use GenServer; def init(_opts), do: {:ok, nil} end
iex> children = [{DynamicSupervisor name: FooApp.ModelInstances}]
iex> opts = [strategy: :one_for_one, name: FooApp.Supervisor]
iex> {:ok, _} = Supervisor.start_link(children, opts)
iex> {:ok, child1} = FooApp.Util.get_or_start_child(
...> FooApp.ModelInstances,
...> %{id: "one", start: {FooApp.Model, :start_link, [[]]}})
iex> {:ok, child2} = FooApp.Util.get_or_start_child(
...> FooApp.ModelInstances,
...> %{id: "two", start: {FooApp.Model, :start_link, [[]]}})
iex> FooApp.Util.get_or_start_child(
...> FooApp.ModelInstances,
...> %{id: "one", start: {FooApp.Model, :start_link, [[]]}})
{:ok, child1} # retrieved the existing child1
"""
@spec get_or_start_child(
dynamic_supervisor :: Supervisor.supervisor(),
child_spec :: Supervisor.module_spec() | Supervisor.child_spec()
) :: DynamicSupervisor.on_start_child()
def get_or_start_child(dynamic_supervisor, child_spec = %{id: id, start: {_, _, _}}) do
with supervisor_pid when is_pid(supervisor_pid) <- GenServer.whereis(dynamic_supervisor) do
# https://hexdocs.pm/elixir/1.18/GenServer.html#module-name-registration
# FIXME: https://hexdocs.pm/elixir/1.18/Registry.html#module-using-in-via
reg_name = {:global, {supervisor_pid, id}}
# https://github.com/elixir-lang/elixir/blob/v1.18.2/lib/elixir/lib/dynamic_supervisor.ex#L434-L477
child_spec =
Map.update!(child_spec, :start, fn
{m = GenServer, f = :start_link, [m1, init_arg]} ->
{m, f, [m1, init_arg, [name: reg_name]]}
{m = GenServer, f = :start_link, [m1, init_arg, opts]} ->
{m, f, [m1, init_arg, opts ++ [name: reg_name]]}
{m, f = :start_link, [init_arg, opts]} when is_list(opts) ->
#true = GenServer in Keyword.get(m.__info__(:attributes), :behaviour, [])
{m, f, [init_arg, opts ++ [name: reg_name]]}
{m, f = :start_link, [opts]} when is_list(opts) ->
#true = GenServer in Keyword.get(m.__info__(:attributes), :behaviour, [])
{m, f, [opts ++ [name: reg_name]]}
_start ->
raise ArgumentError, """
only GenServer style start_link children supported at this time.
\tchild_spec = #{inspect(child_spec)}\
"""
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(supervisor_pid, child_spec) do
{:error, {:already_started, pid}} -> {:ok, pid}
result -> result
end
else
{name, node} = server when is_atom(name) and is_atom(node) ->
# https://github.com/elixir-lang/elixir/blob/v1.12.2/lib/elixir/lib/gen_server.ex#L1213
raise ArgumentError,
message: """
Only local supervisors supported at this time.
\tserver = #{inspect(server)}\
"""
nil ->
# https://github.com/elixir-lang/elixir/blob/v1.12.2/lib/elixir/lib/gen_server.ex#L1205
{:error, :noproc}
end
end
def get_or_start_child(dynamic_supervisor, module) when is_atom(module) do
# https://github.com/elixir-lang/elixir/blob/v1.12.2/lib/elixir/lib/gen_server.ex#L915
get_or_start_child(dynamic_supervisor, module.child_spec([]))
end
def get_or_start_child(dynamic_supervisor, {module, arg}) when is_atom(module) do
get_or_start_child(dynamic_supervisor, module.child_spec(arg))
end
end
@James-E-A
Copy link
Author

James-E-A commented Dec 6, 2024

realistically, we need a module, DynamicSupervisor2, which has a more powerful internal state than just a PID-keyed map

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment