Конфликт дерева наблюдения в приложении umbrella

#elixir #umbrella

#elixir #umbrella

Вопрос:

Предыстория

У меня есть приложение umbrella, в котором много приложений меньшего размера. Одно из этих приложений, called A , должно иметь возможность запускать и контролировать другое приложение called B .

B будучи самостоятельным приложением, предоставляет общедоступный API и имеет GenServer, отвечающий за получение запросов, которые затем перенаправляются на логические модули и тому подобное.

Проблема

Итак, у меня есть два требования:

  1. Я должен иметь возможность запускать B независимо и работать как обычное автономное приложение.
  2. A должен иметь возможность иметь B в своих дочерних элементах и перезапускать / управлять им, если возникнет такая необходимость.

Проблема, с которой я сталкиваюсь здесь, заключается в том, что с моим кодом я могу достичь либо 1, либо 2, но не обоих.

Код

Итак, ниже приведен важный код для приложения 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 (упрощенный)

 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
 

И вот точка входа B

b.пример

 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 по сути, это фасад для сервера с некоторой дополнительной контекстной информацией, такой как спецификации, определения типов и т. Д. (Здесь опущено для краткости).

Как A управлять жизненным циклом?

Насколько я понимаю, деревья наблюдения указаны в application.ex файле приложений. Итак, насколько я понимаю, я создал этот файл приложения для 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
 

Который должен работать, за исключением того, что это не так.

Когда внутри A папки, если я запускаю iex -S mix , вместо хорошего запуска я получаю следующую ошибку:

 ** (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>
 

Мое текущее понимание проблемы заключается в том, что A файл application.ex конфликтует с файлом B приложения.

Вопросы

  1. Как мне исправить этот конфликт?

Ответ №1:

Если я правильно понял требование, A хочет в конечном итоге остановить B приложение и B процесс возрождения контролируется,

Application.stop/1 делает именно это.

 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
 

Обратите внимание, что ожидается имя приложения, как в mix.exs файле (и позже в ×××.app файле после компиляции).)

Комментарии:

1. A не хочет останавливать B и перезапускать его. A хочет запустить B и контролировать его как часть своего дерева наблюдения. Проблема здесь в том, что при A попытке сделать это он взрывается, потому B что уже был запущен (потому B что это само по себе зонтичное приложение). Такое поведение противоречиво, и я не уверен, что это архитектурная проблема из моего приложения umbrella, или если в моем решении чего-то не хватает.

2. После долгих размышлений об архитектуре моего проекта я пришел к выводу, что это неверно и нуждается в изменении. Мое приложение B должно быть библиотекой с полным состоянием, а не приложением elixir. Однако, поскольку ваш ответ технически исправит проблему, я приму его. Спасибо, что нашли время, чтобы прочитать мой пост!