Getting the right Application Enviroinment in an Umbrella Project?

I’m experiencing an unexpected behaviour when accessing the application environment in an umbrella project with multiple children apps. It seems that Application.get_env/3 function doesn’t return the value stored in the config/config.exs of the child application where the function is run. Instead it returns the valued stored in the config/config.exs of the most recently added child app in the umbrella project.

To better test out the problem I’ve create a playground umbrella project adding some very basic Phoenix 1.3 apps: a shared context app (my_app) and some web apps (my_app_web_one, my_app_web_two, …)

    $ mix new my_umbrella --umbrella  && cd my_umbrella/apps
    $ mix phx.new.ecto my_app
    $ mix phx.new.web my_app_web_one 
    $ mix phx.new.web my_app_web_two

and made these adjustements on the scaffold files:

    my_umbrella
    └── apps
        │
        ├── my_app            		 ## The shared Ecto/Context app
        │
        │
        │
        ├── my_app_web_one          ## The first web app
        │   ├── config
        │   │   └── config.exs	       # 1) changed to    config :my_app_web_one, ..., ecto_repos: [MyApp.Repo]  
        │   │                        # 2) changed to    config :my_app_web_one, :generators, context_app: :my_app  
        │   │                        # 3) add entry for config :mylib, endpoint: MyAppWebOne.Endpoint
        │   │
        │   ├── test
        │   │   ├── my_app_web_one
        │   │   │   └── my_test.exs          # Add test for     assert Application.get_env(:mylib, :endpoint) == MyAppWebOne.Endpoint
        │   │   ├── support 
        │   │   │       ├── channel_case.exs # Changed occurrency of {MyAppWebOne.Repo} to {MyApp.Repo}
        │   │   │       └── conn_case.exs    # Changed occurrency of {MyAppWebOne.Repo} to {MyApp.Repo}     
        │   │   └── test_helper.exs          # Changed occurrency of {MyAppWebOne.Repo, :manual} to {MyApp.Repo, :manual}
        │   │
        │   └── mix.exs               # 1) add {:my_app, in_umbrella: true} to deps
        │                             # 2) add {:mylib, "~> 1.0"} to deps
        │
        │
        │
        └── my_app_web_two          ## The second web app
            ├── config
            │   ├── config.exs	       # 1) changed to    config :my_app_web_two, ..., ecto_repos: [MyApp.Repo]  
            │   │                    # 2) changed to    config :my_app_web_two, :generators, context_app: :my_app  
            │   │                    # 3) add entry for config :mylib, endpoint: MyAppWebTwo.Endpoint
            │   └── dev.exs	       # Changed the http port to 4040
            │
            ├── test
            │   ├── my_app_web_one
            │   │   └── my_test.exs          # Add test for     assert Application.get_env(:mylib, :endpoint) == MyAppWebTwo.Endpoint
            │   ├── support 
            │   │       ├── channel_case.exs # Changed occurrency of {MyAppWebTwo.Repo} to {MyApp.Repo}
            │   │       └── conn_case.exs    # Changed occurrency of {MyAppWebTwo.Repo} to {MyApp.Repo}     
            │   └── test_helper.exs          # Changed occurrency of {MyAppWebTwo.Repo, :manual} to {MyApp.Repo, :manual}
            │
            └── mix.exs                      # 1) add {:my_app, in_umbrella: true} to deps
                                             # 2) add {:mylib, "~> 1.0"} to deps

The adjustements mainly regard setting the right references for the context app in the web applications, and configuring a dependency called mylib.

the mylib entry for apps/my_app_web_one/config/ config.exs is

    config :mylib,
        endpoint: MyAppWebOne

while the mylib entry for apps/my_app_web_two/config/ config.exs is

    config :mylib,
        endpoint: MyAppWebTwo

And these are the tests for checking the value returned by Application.get_env in each web app :

for apps/my_app_web_one:

# (the namespace is of course different for each app)
defmodule MyAppOne.MyTest do
  use ExUnit.Case

  test "test 'mylib' endpoint for MyAppOne" do
    assert Application.get_env(:mylib, :endpoint) == MyAppOne.Endpoint
  end
end

for apps/my_app_web_two:

defmodule MyAppTwo.MyTest do
  use ExUnit.Case

  test "test 'mylib' endpoint for MyAppTwo" do
    assert Application.get_env(:mylib, :endpoint) == MyAppTwo.Endpoint
  end
end

Running the tests for each web app give:

  • test for apps/my_app_web_one fails because Application.get_env(:mylib, :endpoint) returns MyAppTwo.Endpoint instead of MyAppOne.Endpoint
  • test for apps/my_app_web_two succeded.

Adding a third web app to the project called my_app_web_three, things go this way instead:

  • test for apps/my_app_web_one fails because Application.get_env(:mylib, :endpoint) returns MyAppThree.Endpoint instead of MyAppOne.Endpoint
  • test for apps/my_app_web_two fails because Application.get_env(:mylib, :endpoint) returns MyAppThree.Endpoint instead of MyAppTwo.Endpoint
  • test for apps/my_app_web_three succeded

I’ve also tried to leave just one web app at time in the umbrella project, and test succeded every time for all the three apps.

I wasn’t able to find in the documentation for Elixir umbrella projects, for Application.get_env/3 or for the Phx generators anything that let me understand where can be the problem, neither googling on this forum or on stackoverflow.

So maybe is just me that don’t understand well how things have to work in these kinds of projects. Can you point me towards something I’ve missed?

The relevant information is here (look in the examples section where they run config :lager ... twice). You are overwriting the :endpoint key for :mylib in each successive config.

Look in config/config.exs in the root of your umbrella project.

There you will find:

import_config "../apps/*/config/config.exs"

What’s happening:

config :mylib, endpoint: MyAppWebOne.Endpoint
config :mylib, endpoint: MyAppWebTwo.Endpoint # overwritten! Application.get_env(:mylib, :endpoint) == MyAppWebTwo
config :mylib, endpoint: MyAppWebThree.Endpoint # overwritten! Application.get_env(:mylib, :endpoint) == MyAppWebThree

You must namespace each config or you will overwrite :endpoint. Usually the convention would be to do

config :my_app_one, endpoint: MyAppOne.Endpoint
config :my_app_two, endpoint: MyAppTwo.Endpoint
config :my_app_three, endpoint: MyAppThree.Endpoint

Oh yeah, I see, thank you, now I understand why the values are overwrittten!

During my experiments I had tried to namespace each config and I saw that it worked. But this doesn’t fix the problem because Application.get_env(:thelib, :endpoint) is actually called from inside the thelib that is a dependency used in all the children apps. This library is an external library, it is not under my control. It have to be namespace-agnostic as it looks for its standard name when it run the Application.get_env(:thelib, :endpoint) function to retrieve the peculiar endpoint for the web app that it is using the library.

(In the above samples I’ve run the test in the child app just to simplify my debug, not because I’ve actually need to do it there)

How did you solve this in the end, I have the same issue in my umbrella project?

Unfortunately it seems that there isn’t a solution directly applicable to a child app. Because of the overriding mechanism behind Mix.Config.config/2, the problem has to be addressed by the developer of the external library. A non-umbrella-aware library expects to get all its parameters from the key/values in the config/config.exs file. Instead, an umbrella-aware library is designed to expect to receive the parameters specific for a certain child app as arguments passed to its api from the child app, and only the general parameters (parameters that has/can be the same for all children apps) as the key/values in the config/config.exs file.

@gts do you have a link to any information on writing umbrella aware libraries?

1 Like

No sorry, I haven’t any significant link. But I can share the experience I acquired trying to solve the problem answering to any more specific questions

1 Like