James_E

James_E

Correctness/safety question with DynamicSupervisor

I have an app that has many users connecting to it.

When each user tries to connect to a room, they should be connected to a “MyApp.Room” GenServer, which is started if a room with the requested ID does not yet exist.

All of the “MyApp.Room” instances are managed by one app-wide DynamicSupervisor.

Unfortunately, DynamicSupervisor just records all children in a PID-keyed map with no method to retrieve a particular child in any way except by PID, so I wrote a function like this to try to implement that “started if…does not yet exist” logic:

  @doc """
  Exactly like [DynamicSupervisor.start_child/2](https://hexdocs.pm/elixir/1.19/DynamicSupervisor.html#start_child/2)
  except that the "id" field is not [disregarded](https://hexdocs.pm/elixir/1.19.0-rc.0/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} = get_or_start_child(
      ...>   FooApp.ModelInstances,
      ...>   %{id: "one", start: {FooApp.Model, :start_link, [[]]}})
      iex> {:ok, child2} = get_or_start_child(
      ...>   FooApp.ModelInstances,
      ...>   %{id: "two", start: {FooApp.Model, :start_link, [[]]}})
      iex> 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, module) when is_atom(module) do
    # https://github.com/elixir-lang/elixir/blob/v1.19.0-rc.0/lib/elixir/lib/gen_server.ex#L1036
    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
  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.19/GenServer.html#module-name-registration
      # FIXME: https://hexdocs.pm/elixir/1.19/Registry.html#module-using-in-via
      reg_name = {:global, {supervisor_pid, id}}

      # https://github.com/elixir-lang/elixir/blob/v1.19.0-rc.0/lib/elixir/lib/dynamic_supervisor.ex#L407-L490
      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/erlang/otp/blob/84adefa331c4159d432d22840663c38f155cd4c1/lib/stdlib/src/gen.erl#L71
      # https://github.com/elixir-lang/elixir/blob/v1.19.0-rc.0/lib/elixir/lib/gen_server.ex#L1050
      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.19.0-rc.0/lib/elixir/lib/gen_server.ex#L1324-L1326
        raise ArgumentError,
          message: """
          Only local supervisors supported at this time.
          \tserver = #{inspect(server)}\
          """

      nil ->
        # https://github.com/elixir-lang/elixir/blob/v1.19.0-rc.0/lib/elixir/lib/gen_server.ex#L1309
        {:error, :noproc}
    end
  end

However, this seems fragile; I’m wondering in particular whether, as written, it contains any race conditions, or if it’d be safe to call the current version of get_or_start_child blindly from, for example, a Phoenix LiveView mount callback.

If this approach is subject to a race condition, is there any correct way to accomplish this instead?

Most Liked

josevalim

josevalim

Creator of Elixir

We have just rewritten the Mix & OTP guides in Elixir to build something similar, you can read it in this PR. We also built something that relied on similar techniques here: Homemade analytics with Ecto and Elixir - Dashbit Blog

windexoriginal

windexoriginal

I think you should look at the registry module. It will allow you to give arbitrary identifiers to the processes you spawn dynamically.

Registry is implemented with ets which should help with concerns about race conditions.

josevalim

josevalim

Creator of Elixir

The updated docs are here: Simple state with agents — Elixir v1.21.0-dev

We generally treat naming processes and supervising processes as separate things. You don’t need to wrap the registry and supervisor access from within the agent. The architecture built in the getting started guide should be enough and provide uniqueness (either locally or in a cluster).

Where Next?

Popular in Questions Top

Tee
can someone please explain to me how Enum.reduce works with maps
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New

Other popular topics Top

New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New

We're in Beta

About us Mission Statement