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://elixirforum.com/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