Last active
February 10, 2025 17:50
-
-
Save James-E-A/7a8393603870c9d3a2f1903bc8dead82 to your computer and use it in GitHub Desktop.
Elixir DynamicSupervisor start child if it doesn't already exist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
realistically, we need a module,
DynamicSupervisor2
, which has a more powerful internal state than just a PID-keyed map