vschroeder

vschroeder

How to start a GenServer-based Application before other Applications?

Hey there!

This probably has a very easy answer, but I’m already too many hours into this and need some help. In this scenario, we have several separated git repositories, one being a library to provide configuration values and the others are applications that use the mentioned lib.

The library used to be a plain stateless module and this has been working well for years. Recently, we had to change this library to include some caching and different data sources, which caused a GenServer to be added. It means that it ceased to be a simple bunch of functions and now requires to be fully loaded and started to be used.

One peculiarity, being a config provider, this library is usually invoked from start/2 callbacks of the other Applications, hence it has to be up and running before the other applications have their start/2 functions called.

The (greatly simplified) structure is as follows:

Library: mix.exs

defmodule Foo.Config.MixProject do
  use Mix.Project

  def project do
    [
      app: :foo_config,
      version: "2.0.0",
      elixir: "~> 1.10",
      deps: [{:jason, "~> 1.2"}],
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mods: {Foo.Config.Application, []},
    ]
  end
end

Library: lib/application.ex

defmodule Foo.Config.Application do
  @moduledoc false
  use Application

  @impl true
  def start(_type, _args) do
    children = [Foo.Config.Server]
    opts = [strategy: :one_for_one, name: Foo.Config.Supervisor]
    Supervisor.start_link children, opts
  end
end

Library: lib/config.ex

defmodule Foo.Config do
  alias Foo.Config.Server

  def get_config(key_path) when is_binary(key_path) do
    GenServer.call Server.name, {:get_config, key_path}
  end
end

Library: lib/server.ex

defmodule Foo.Config.Server do
  use GenServer
  require Logger

  @server_name {:global, __MODULE__}

  def name, do: @server_name

  def start_link(_opts) do
    GenServer.start_link __MODULE__, [], name: @server_name
  end

  def child_spec(_args) do
    %{
      id: Foo.Config.Server,
      start: {Foo.Config.Server, :start_link, [nil]},
    }
  end

  @impl GenServer
  def init(_opts) do
    # Some operations to load initial state, potentially expensive
    {:ok, %{config: initial_state}}
  end

  @impl GenServer
  def handle_call({:get_config, key_path}, _from, %{config: data} = state)
    when is_binary(key_path) and is_map(data)
  do
    # Uses private functions to calculate the value to be returned
    {:reply, value, state}
  end
end

Application: mix.exs

defmodule Bar.MixProject do
  use Mix.Project

  def project do
    [
      app: :bar,
      version: "2.1.0",
      elixir: "~> 1.10",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      test_paths: test_paths(Mix.env),
      releases: [
        bar: [
          include_executables_for: [:unix]
        ]
      ]
    ]
  end

  def application do
    [
      extra_applications: [
        :logger,
        # We tried to add the library here, but didn't help
        :foo_config,
      ],
      mod: mods(Mix.env),
    ]
  end

  defp mods(:unit), do: []
  defp mods(_all), do: {Bar.Application, []}

  defp test_paths(:integration), do: ["test/integration"]
  defp test_paths(:unit), do: ["test/unit"]
  defp test_paths(_), do: []

  defp deps do
    [
      {:argon2_elixir, "~> 2.3"},
      {:joken, "~> 2.2"},
      {:mongodb_driver, "~> 0.7"},
      {:plug_cowboy, "~> 2.2"},
      {
        :foo_config,
        git: "git@github.com:my-company/foo-config.git",
        tag: "v2.0.0",
      },
    ]
  end
end

Application: lib/bar/application.ex

defmodule Bar.Application do
  use Application

  # We tried to use `require` here, but no effect
  require Foo.Config

  import Foo.Config, only: [
    get_config: 1,
    get_secret: 1,
  ]

  require Logger

  def start(_type, _args) do
    # We also tried the lines here, but no luck:
    # {:ok, _} = Application.ensure_all_started :foo_config
    # Process.sleep 5000

    {:ok, port} = get_config "port"
    {:ok, db_url} = get_config "uri"
    {:ok, db_user} = get_config "username"
    {:ok, db_pass} = get_config "password"

    children = [
      {Plug.Cowboy, [
        scheme: :http,
        plug: Bar.Router,
        options: [ip: {0, 0, 0, 0}, port: port],
      ]},
      {Mongo, [
        name: :db,
        appname: "Bar",
        url: db_url,
        username: db_user,
        password: db_pass,
        # More config here, obtained through `get_config/1`
      ]},
    ]

    opts = [name: Bar.Supervisor, strategy: :one_for_one]
    Logger.info "Starting Bar.Supervisor"
    Supervisor.start_link children, opts
  end
end

Application: many other files, such as router.ex

defmodule Foo.Router do
  use Plug.Router
  # Lots of magic here
end

Compilation runs just fine, but trying to run the application always results in the following error (extracted from docker-compose logs). The CMD for this container during development is mix do deps.get, run --no-halt and bar start for the release in production:

app_1          | ** (Mix) Could not start application bar: exited in: Bar.Application.start(:normal, [])                                                              
app_1          |     ** (EXIT) exited in: GenServer.call({:global, Foo.Config.Server}, {:get_config, "port"}, 5000)                                                            
app_1          |         ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started

What am I missing here? Suggestions about project structure are also welcome, I just really need to keep the config provider as an independent library because many different applications (deployed as independent Erlang nodes and containers) are using the same logic.

Thanks in advance! Live long and prosper \\//_

Marked As Solved

al2o3cr

al2o3cr

The mix compile.app docs mention a mod: (singular!) option, not a mods:.

Where Next?

Popular in Questions Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
gonzofish
I’m currently trying to understand how to join three tables using Ecto. All the examples I’ve seen use 2, so maybe I’m just missing somet...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

Other popular topics Top

yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New

We're in Beta

About us Mission Statement