I have an umbrella app that has many smaller apps inside. One of this apps, called A
, needs to be able to spin and supervise another app, called B
.
B
, being an app in its own right, exposes a public API and has a GenServer, responsible for receiving requests that it then redirects to the logic modules and such.
So, I have two requirements:
B
independently and have it work as a normal standalone app.A
must be able to have B
in its children and restart/manage it, should such a need arise.The problem I have here, is that with my code I can either achieve 1 or 2, but not both.
So, the following is the important code for app B
:
application.ex
defmodule B.Application do
@moduledoc false
use Application
alias B.Server
alias Plug.Cowboy
@test_port 8082
@spec start(any, nil | maybe_improper_list | map) :: {:error, any} | {:ok, pid}
def start(_type, args) do
# B.Server is a module containing GenServer logic and callbacks
children = children([Server])
opts = [strategy: :one_for_one, name: B.Supervisor]
Supervisor.start_link(children, opts)
end
end
server.ex (simplified)
defmodule B.Server do
use GenServer
alias B.HTTPClient
#############
# Callbacks #
#############
@spec start_link(any) :: :ignore | {:error, any} | {:ok, pid}
def start_link(_args), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
@impl GenServer
@spec init(nil) :: {:ok, %{}}
def init(nil), do: {:ok, %{}}
@impl GenServer
def handle_call({:place_order, order}, _from, _state), do:
{:reply, HTTPClient.place_order(order), %{}}
@impl GenServer
def handle_call({:delete_order, order_id}, _from, _state), do:
{:reply, HTTPClient.delete_order(order_id), %{}}
@impl GenServer
def handle_call({:get_all_orders, item_name}, _from, _state), do:
{:reply, HTTPClient.get_all_orders(item_name), %{}}
##############
# Public API #
##############
def get_all_orders(item_name), do:
GenServer.call(__MODULE__, {:get_all_orders, item_name})
def place_order(order), do:
GenServer.call(__MODULE__, {:place_order, order})
def delete_order(order_id), do:
GenServer.call(__MODULE__, {:delete_order, order_id})
end
And here is the entrypoint of B
b.ex
defmodule B do
@moduledoc """
Port for http client.
"""
alias B.Server
defdelegate place_order(order), to: Server
defdelegate delete_order(order_id), to: Server
defdelegate get_all_orders(item_name), to: Server
@doc false
defdelegate child_spec(args), to: Server
end
b.ex
is basically a facade for the Server, with some extra context information such as specs, type definitions, etc (omitted here for the sake of brevity).
A
manage the lifecycle?It is my understanding that supervision trees are specified in the application.ex
file of apps. So, from my understanding, I have created this application file for A
:
defmodule A.Application do
@moduledoc false
use Application
alias B
def start(_type, _args) do
children = [B]
opts = [strategy: :one_for_one, name: A.Supervisor]
Supervisor.start_link(children, opts)
end
end
Which should work, except it doesn't.
When inside A
's folder, if I run iex -S mix
, instead of having a nice launch I get the following error:
** (Mix) Could not start application a: A.Application.start(:normal, []) returned an error: shutdown: failed to start child: B.Server
** (EXIT) already started: #PID<0.329.0>
My current understanding of the issue is that A
's application.ex file is conflicting with B
's application file.
If I understood the requirement correctly, A
wants to ultimately stop B
application and respawn B
process supervised,
Application.stop/1
is doing exactly this.
defmodule A.Application do
...
def start(_type, _args) do
Application.stop(:b) # ⇐ THIS
children = [B]
opts = [strategy: :one_for_one, name: A.Supervisor]
Supervisor.start_link(children, opts)
end
end
Note it expects the name of the application, as in mix.exs
file (and later in the ×××.app
file after compilation.)