Search code examples
elixirumbrella

Supervision tree conflict in an umbrella app


Background

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.

Issue

So, I have two requirements:

  1. I must be able to launch B independently and have it work as a normal standalone app.
  2. 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.

Code

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).

How does 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.

Questions

  1. How do I fix this conflict?

Solution

  • 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.)