Qqwy

Qqwy

TypeCheck Core Team

On Configuration: Can we improve how Elixir libraries/applications are configured?

Recently, at the Code BEAM Lite Amsterdam, I had very interesting conversations with @sasajuric, @michalmuskala, @voltone, @Crowdhailer and a couple of other people. Amongst the subjects was the concept of configuration, and choices to be made here.

Most importantly, @sasajuric’s talk had some interesting points:

  • When you decide to do something yourself (in his talk: building a Continuous Integration system), you can have a very focused implementation that does not need a ‘general purpose’ configuration layer with all bells and whistles.
  • Configuration just as passing arguments to functions is infinitely more flexible than config files.
  • In a system where everything is done in the same language, there is no difference between DevOps and Programmer.

And on the other hand, the talk by @voltone:

  • explained the bad experience of configuring Erlang’s built-in :ssl-library properly. (it’s default configuration is very unsafe).
  • mentioned that many of the HTTP(S) client libraries out there either require the programmer using them to pass on the proper :ssl-configuration, or the few that do use a secure configuration by default will completely replace it if the programmer passes in their own.

So, while about a year ago we had a topic talking about how to configure your application and libraries you are building, I think it is time to look at this subject anew.


There are roughly three ways to configure a bit of functionality (whether a library, an OTP-application or something else) in Elixir land:

  1. Pass options as argument(s) to a function. This is the most flexible, because it allows calling the function from many places in your code with different options. Elixir makes this approach easier by having keyword-lists as first-class type.
  2. Add configuration settings in the config/config.exs, config/some_mix_environment.exs and similar files. This makes it possible to have different configuration on different environments (for instance, on your local computer the database has a different password than on the production machine). However, it is not possible to use functionality that only can be configured like this in multiple different ways within the same environment.
  3. Configure your application using environment variables, and use some way to read these in your application’s code. The nice part of this approach is that you can have different settings on different machines (even if they are both ‘production’). The bad part is that you currently need a special way to make sure these are inserted in your application’s configuration.

Problems with (2) config.exs-style configuration and (3) environment variables.

Configuration happens at build time

(2) and (3) have the extra catch that when you build your application, for instance when building a release using a tool like Distillery, the configuration is read during building rather than when ran. This means that you either need to build on the machine that will run the release, or your need to take extreme care to make sure the configuration values are still relevant.

Ad-hoc, turing complete configuration.

Furthermore, values filled in in (2) and (3) very much affect the functionality you use in an implicit and ad-hoc way: From looking at the code itself it is impossible to see how it is configured (or that it even has special configuration). Also, because config.exs-style files are essentially just normal Elixir files, they might contain a spaghetti of arbitrary Turing-complete nonsense; most commonly some files override some settings of some other files, making it more difficult to see which configuration will actually take place in the final application (without running Application.get_env manually from within the running application.)

Problems with (1) functions that have option-arguments

Now, (1) is definitely the most flexible. Most importantly, we can configure it differently in each and every one of our tests.

No standardized way of checking the current application environment

However, something that does stick out like a sore thumb is that there does not seem to be a standardized way to check inside your code in which (Mix) environment you currently are: Mix.env will not work inside a built application, because the :mix OTP application will not be included.
Instead, it’s documentation actually encourages you to use (2) to configure environment-specific configuration.

Maybe this is something that we could improve?

Options-handling via keyword-args is somewhat clunky, and could be improved

  • We cannot match on these in the function header, because keyword args are ‘just lists’ and therefore position-dependent: def foo(a, b, c: d, e: f) will not be triggered when someone calls foo(1,2,d: 3, c: 4).
  • In almost all cases, duplicate option entries are irrelevant. However, there is no way to extract a map directly from the options at the end.

So almost all option-handling code goes a bit like this:

  • options are passed in as keyword list
  • we either combine (in some sensible way) or warn/crash on duplicate entries (this is very frequently ommitted by libraries in the wild!)
  • these are then transformed into a map (or sometimes a special options-struct)
  • we fill in sensible defaults for the options that were not provided. (which sometimes depend on yet other options that were provided).

I think a library (or potentially, since it is such a common thing to do, either a ‘blessed’ library or even inclusion into Elixir’s standard lib!) that makes it easier to do properly; essentially I a thinking about something similar to Python’s argparse module (for ARGV parsing), where a specification for the options (including field names, descriptions, allowed types and defaults) can be used for parsing, warning/crashing on error as well as generating documentation of the allowed options!

Problems in general

Libraries restrict their user

Libraries currently commonly pick either (1) or (2) (sometimes with the possibility for (3)).

I think it would be a lot better if libraries were to use:

  • default values
  • overriding these per key with what you configured using (2)/(3)
  • overriding these per key with what you configured in (1).

Alas, possibly in part because there is not a standardized way to do this currently, many libraries either restrict you to one of these approaches, or completely their default configuration out the window once you start customizing something.
In certain cases, such as the SSL-client libraries, this can have disastrous consequences unless the user of the library is aware (the configuration behaviour of a library is almost always ‘implicit’ and not mentioned anywhere).

And related: libraries could warn if they encounter options that they do not recognize, but currently usually do not AFAIK.


So, those are my current thoughts on the matter. I think we can improve this current situation as a community. In this post, I gave some suggestions that I currently think might be worth exploring, but I am also very interested in your suggestions and opinions about this matter.

Most Liked

rvirding

rvirding

Creator of Erlang

This is really an Elixir problem as in Erlang systems config files are read at start-up time. So using configuration files to set application environment variables is the standard way of configuring applications. You also have the extra option of sett in environment variables on the command line as well. Why this is not done this way in Elixir I don’t know, or has it been fixed in latest version of distillery?

tim2CF

tim2CF

Hi
We had a few discussions inside our company how to deal with compiletime/runtime configs and we decided to completely split these things and at the same time avoid system variables mess. This library partly solves issues you initially mentioned

https://github.com/coingaming/ex_env

OvermindDL1

OvermindDL1

That is the big lock-in though. Basically what I said in the old thread I still think holds true. We need to multiple stages of config’s, those that exist at compile time that act to adjust code generation and act as defaults for later stages, those that exist at start-up only, which act as defaults for later stages but otherwise should only be things that are absolutely only necessary at start-up time (things like BEAM settings, this stage is mostly just for setting default values for the runtime stage), and those that exist at runtime, which allows for dynamically changing them in a running system, application environment is this for example, however there needs to be a way for when part of the application environment is changed that a signal/message is sent to whatever registered handlers so they can update based on the new configuration changes (like increasing/decreasing the number of SQL pool workers as one example). This pattern should be ubiquitous so everything is consistent through-out the ecosystem and thus needs to be baked into Elixir itself.

This is all of course adjacent but related to ‘where’ should the configurations come from, which I also spoke of on that other thread, basically need a set of handlers to pull settings from, whether one for environment variables, one from some data store, one to pull from a database (even registering a postgres listener for example to listen to setting changes from the appropriate database table or so), xml files, json, whatever, it needs to be pluggable, Java does this very well (though antiquated compared to better ideals now) as one big example.

Where Next?

Popular in Discussions Top

marciol
Please, let me know if this kind of discussion already took place in another topic . Hi all, how do you consider if is better to build ...
New
WolfDan
After doing a port from a c++ library to my project in phoenix I’ve seen that I need a faster way to run this algorithm and I found this ...
New
sashaafm
I’m trying to evaluate the best combo/stack for a BEAM Web app. Right now I’m exploring Yaws a bit, after having dealt with Phoenix for a...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
thojanssens1
It would be nice to be able to define a redirect from one route to another from the router.ex file. E.g.: redirect "/", UserController, ...
New
lorenzo
Hey everone! I created a prototype for my app using Nodejs for the api. But the framework I chose wasnt great (in general theresnt any g...
New
RudManusachi
What configs will make sense to put to runtime.exs? – A bit of how I configure apps: I have generic configs in config/config.exs, dev...
New
acrolink
How does the two languages compare when it comes to server side application development? Any experiences or ideas? Thank you.
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
Qqwy
Looking at the stacks that existing large companies have used, WhatsApp internally uses Mnesia to store the messages, while Discord uses ...
New

Other popular topics Top

siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
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
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement