James_E

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_spec is discarded and this function returns an error with :already_started or :already_present if 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

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

al2o3cr

The docs for DynamicSupervisor.start_child mention that the id is required but ignored:

Note that while the :id field is still required in the spec, the value is ignored and therefore does not need to be unique.

dimitarvp

dimitarvp

Come on now, spec is not “wack”, you just need a name if you want to, you know, have a named process. :smiley:

James_E

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.

Where Next?

Popular in Questions Top

Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
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
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement