Soren

Environment Reloading

One of the core principals of The Twelve-Factor App, and a highlight of deploying applications on Heroku, is storing configuration in the environment:

Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.

In the Ruby world the Dotenv library makes it simple to dynamically load configuration from values stored in local .env files. Early in the loading process the file is read and each key value pair is loaded into Ruby's hash-like ENV object. A common, and simple, example of using environment variables is storing the URL, credentials, and configuration for a database connection:

DATABASE_URL=postgres://username:password@localhost:port/database?pool=16

The details of the connection are confidential and shouldn't be checked into source control. An .env file can be managed independently of the source code and transferred to the web server securely, even as part of the deploy process. This method of providing database configuration is so common that Rails will check the ENV for a DATABASE_URL when it boots. This built in usage of environment variables is great, but there are some caveats.

Forking

Those familiar with Heroku know that when you change an environment variable on Heroku, no matter how small, the application will be restarted. In the land of Heroku a restart means creating all new containers for your application, starting them up, and finally routing traffic to them once they have loaded. Complete shutdown and startup is consistent, but has noticeable lag when compared to the hot reloading available in Unicorn or Puma.

Unicorn servers achieve concurrency by running one or more workers, each controlled from a single master process. The master process listens for Unix signals such as TERM, QUIT, or USR2 and manages the pool of workers accordingly. For example, when the master receives a USR2 signal it forks new worker instances with the most recent version of the code and begins directing connections to the new instances. This is called a phased restart.

Starting a new Unicorn master forks it from the current process, typically a shell of some kind. During the forking process it inherits the shell's environment variables. After the process is forked it loses any reference to the shell's environment, so any further changes to the environment will be ignored. This separation prevents chaos between different processes, but it also creates a hiccup when we want to update the configuration for a long running process like Unicorn.

Updating Configuration

Updating environment variables from a configuration file can be performed at any time with ENV.update. Calling update will add or replace any existing keys with the new values, but only within the current process. In order to have the updated ENV cascade down to the workers actually handling requests we have to call update before the workers are forked. It is very common to perform some setup around the exec/fork life cycle, so servers provide life cycle hooks. Here is an example of how to update within a Unicorn config:

require 'dotenv'

before_exec do
  ENV.update Dotenv::Environment.new('.env')
end

Or, alternately, within a Puma config:

require 'dotenv'

on_worker_boot do
  ENV.update Dotenv::Environment.new('.env')
end

With the configuration hooks in place you can safely update a .env file at any time, issue a restart, and change configuration on the fly.