Best way to do synchronized start between umbrella apps using start_phases

I’m scratching my head on how to accomplish sync startup between umbrella apps, but no luck right now.

The base erlang reference (what I’m trying to obtain) is: Erlang -- Included Applications

I can use start_phases inside a single app to do things after the start callback and before the start returns , but I’m not able to sync several apps like the erlang reference does.

I’ve played with included_applications, extra and so on, but no luck.

Here’s a sample repo for what I’m testing: GitHub - xadhoom/sync_start_test: testing elixir umbrella apps synced start

What I’m obtaining is:

Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

SECONDARY: :go
PRIMARY: :init
PRIMARY: :go
Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

While I’m expecting to have: init & go from the primary app and then go from the secondary.
Maybe this is not (yet) possible with elixir?

1 Like

uhm, seems that the generated .app file is wrong.

According to erlang docs, if a primary app has an included app, the callback module should be:

{mod, {application_starter,['Elixir.Primary.Application',[]]}} (only in the primary, including app)

instead of

{mod,{'Elixir.Primary.Application',[]}}

By manually fixing the .app file the startup phases between apps are synced as expected.

I have overlooked something or something must be fixed into the .app generator?

seems something missing to me, so filed an issue https://github.com/elixir-lang/elixir/issues/6533

After some help and pointers from @josevalim I was able to make it work.

The updated repo is always here: https://github.com/xadhoom/sync_start_test and the summary is:

  • create an umbrella containter
  • create your primary app inside the apps dir, as usual. Add some start_phases to it (will the correct callbacks implemened)
  • think about the apps that must be started as included_applications of the primary app. The primary app will become responsible of starting them
  • keep in mind that mix will always start all apps inside the apps/ path
  • create a new folder, like included_apps, at same level of apps/. Example: my_awesome_umbrella/included_apps
  • put into included_apps your secondary app, with a subset of primary app start_phases (remind the callbacks)

Now the setup that makes it work

  • in your primary app mix.exs change the mod value inside application/0 to mod: {:application_starter, [Primary.Application, []]}
  • add an included_application config inside application/0, like included_applications: [:secondary]
  • add a local path dep into deps/0 , like: {:secondary, path: "../../included_apps/secondary"}
  • start your application, you will see start_phases callbacks called in correct order

As @josevalim pointed out, being :secondary out of apps path, the usual mix commands like mix test or other umbrella conscious commands will not run for apps outside the apps/ tree, so they must be run manually.

Caveats:
In order to create new apps inside the included_apps/ path, there’re two possible ways:

  • create a new app using mix new inside the included_apps/ path and adjust all the needed project paths to point to umbrella:
  • build_path: “…/…/_build”,
  • config_path: “…/…/config/config.exs”,
  • deps_path: “…/…/deps”,
  • lockfile: “…/…/mix.lock”,
  • or, more easily, create it under the apps/ path and then move it

Links:

2 Likes

I suspect that you made your life a bit more difficult because you insisted on placing the “top level application” in apps/primary while you placed the included application in included_apps/secondary. When I “ported”
https://github.com/francescoc/scalabilitywitherlangotp/tree/master/ch9/top_app

to Elixir/mix I simply created a regular umbrella project in which the top level application was in umbrella/apps/top_app - side by side with the included app in umbrella/apps/bsc - that way {:bsc, in_umbrella: true} is sufficient inside the the top level applications dependencies:

# File: umbrella/apps/top_app/mix.exs
use Mix.Project

  def project do
    [
      app: :top_app,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      included_applications: [:bsc],                                          # added
      start_phases: [{:start, [5]},{:admin, [4]}, {:stop, [3]}],              # added
      mod: {:application_starter, [TopApp.Application, []]}                   # modified
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
      {:bsc, in_umbrella: true} # added
    ]
  end
end
# File: umbrella/apps/top_app/lib/top_app/application.ex
# Based on
# https://github.com/francescoc/scalabilitywitherlangotp/blob/master/ch9/top_app/top_app.erl
#
# p.229 - "Designing for Scalability with Erlang/OTP Implement Robust, Fault-Tolerant Systems"
#         by Francesco Cesarini and Steve Vinoski (O’Reilly).
#         Copyright 2016 Francesco Cesarini and Stephen Vinoski, 978-1-449-32073-7.
#         http://shop.oreilly.com/product/0636920024149.do
#
defmodule TopApp.Application do
  use Application

  def start(_type, _args),
    do: {:ok, _pid} = BscSup.start_link()

  def start_phase(phase, start_type, phase_args),
    do: IO.puts "top_app:start_phase(#{inspect phase},#{inspect start_type},#{inspect phase_args})."

  def stop(_state),
    do: :ok # same as the default callback

end

# Additions modifications to mix.exs -> application
#
#   included_applications: [:bsc],
#   start_phases: [{:start, [5]},{:admin, [4]}, {:stop, [3]}],
#   mod: {:application_starter, [TopApp.Application, []]}
#
# Additions modifications to mix.exs -> deps
#
#  {:bsc, in_umbrella: true}

# File: umbrella/apps/bsc/mix.exs
defmodule Bsc.Mixfile do
  use Mix.Project

  def project do
    [
      app: :bsc,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps(),
      description: "Base Station Controller" # added
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger],
      mod: {Bsc.Application, []},
      registered: [Bsc.Supervisor, FreqOverload, Frequency, SimplePhoneSup], # added
      env: [my_key: :my_value],                                              # added
      start_phases: [{:init, [2]},{:admin, [1]}, {:oper, [0]}]               # added
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
      # {:sibling_app_in_umbrella, in_umbrella: true},
    ]
  end
end
# File: umbrella/apps/bsc/lib/bsc/application.ex
# Based on
# https://github.com/francescoc/scalabilitywitherlangotp/blob/master/ch9/start_phases/bsc.erl
#
# p.227 - "Designing for Scalability with Erlang/OTP Implement Robust, Fault-Tolerant Systems"
#         by Francesco Cesarini and Steve Vinoski (O’Reilly).
#         Copyright 2016 Francesco Cesarini and Stephen Vinoski, 978-1-449-32073-7.
#         http://shop.oreilly.com/product/0636920024149.do
#
defmodule Bsc.Application do
  use Application

  def start(_type, _args),
    do: BscSup.start_link()

  # p. 227 - mix.exs "application -> start_phases"
  def start_phase(phase, start_type, phase_args),
    do: IO.puts "bsc:start_phase(#{inspect phase},#{inspect start_type},#{inspect phase_args})."

  def stop(_state),
    do: :ok # same as the default callback

end
umbrella/apps/top_app$ iex -S mix
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

top_app:start_phase(:start,:normal,[5]).
top_app:start_phase(:admin,:normal,[4]).
bsc:start_phase(:admin,:normal,[1]).
top_app:start_phase(:stop,:normal,[3]).
Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

Now on a personal note I view the “primary app” as a conductor/orchestrator. For me “primary app + included apps” is the application in the conventional, non-OTP sense because “primary app + included apps” all exist within the same supervision tree - the primary app simply has the privilege/responsibility of controlling the life cycle of the supervision tree.

@peerreynders Uhm, no I was not insisting, it was the only way it worked.

Note that you’re starting from within your top_app folder, if you start it from the top level umbrella project it won’t work.

If I start a similar layout ( GitHub - xadhoom/sync_start_test at normal_umbrella ) from the primary app folder, yep, it works.

But if started from top folder, it will call twice the secondary callbacks (because of how mix starts umbrellas)

Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

SECONDARY: :go
PRIMARY: :init
PRIMARY: :go
SECONDARY: :go
Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

Said that, dunno what can be the best way.
Also thinking about releases of umbrella projects, how the startup sequence will work? I suspect that it will mimic the mix way, so my solution is better, but I’ve not tried it.

Unless there’s someway to handle the startup sequence in the release tools (I still have to investigate them).
If yes, for sure your solution is for sure better, since all “umbrella” enabled mix commands will continue to work :smiley:

1 Like

Still gotta get there - for the time being it’s been a bit of a slog to correlate the low level .app configuration to how mix operates - not expecting .rel to Distillery to be any easier.