Zero-downtime Unicorn restart when using rbenv

There are numerous tutorials that document how to restart Unicorn without dropping any connections, even when Unicorn itself has been updated. The procedure involves sending Unix signals to the Unicorn master:

  1. Send a USR2 signal to the Unicorn master to spawn a new master.
  2. Send a WINCH signal to the old master to gracefully stop workers.
  3. Send a QUIT signal to the old master to gracefully shutdown.

What if I’m using rbenv?

Let’s say you’ve installed Ruby 2.2.1 with rbenv on your production server. Later, Ruby 2.2.2 is released so you build it and update /var/www/my-app/.ruby-version. You try to restart Unicorn using the zero-downtime method above, but sadly the new master continues to use the same version of Ruby as the old master.

What’s going on? Is it possible to restart Unicorn with zero downtime while also migrating to a new version of Ruby?

Fortunately, the answer is yes! However, you’ll need to jump through some hoops since the new master inherits environment variables from the old master. All of the following variables contain the Ruby version and therefore need to be altered before starting the new master:

  • BUNDLE_BIN_PATH
  • GEM_HOME
  • PATH
  • RBENV_VERSION
  • RUBYLIB

We’ll need to use a shim to set the appropriate environment variables before re-executing Unicorn. Below is an example shim. You should amend MY_APP and the exec command to suit your Rails application.

#!/bin/sh

MY_APP="/var/www/my-app"

[ ! -e "$MY_APP/.ruby-version" ] && exit 1
RBENV_VERSION="`/usr/bin/head -n1 $MY_APP/.ruby-version`"
[ -z "$RBENV_VERSION" ] && exit 1
_RUBY_MAJOR_VER="${RBENV_VERSION%.*}.0"

GEM_HOME="$MY_APP/vendor/bundle/ruby/$_RUBY_MAJOR_VER"
_RBENV_RUBY_PATH="$HOME/.rbenv/versions/$RBENV_VERSION"

[ ! -d "$GEM_HOME" ] && exit 1
[ ! -d "$_RBENV_RUBY_PATH" ] && exit 1
[ ! -d "$_RBENV_RUBY_PATH/lib/ruby/gems/$_RUBY_MAJOR_VER/gems" ] && exit 1

# Needed for RUBYLIB and BUNDLE_BIN_PATH.
_get_bundler_path() {
    /usr/bin/find \
        "$_RBENV_RUBY_PATH/lib/ruby/gems/$_RUBY_MAJOR_VER/gems" \
        -mindepth 1 -maxdepth 1 -type d -name 'bundler-[0-9]*' \
        -print -quit || exit 1
}
_BUNDLER_PATH="`_get_bundler_path`"
[ -z "$_BUNDLER_PATH" ] && exit 1
[ ! -d "$_BUNDLER_PATH" ] && exit 1

# Set the necessary environment variables.
PATH="$GEM_HOME/bin"
PATH="$PATH:$_RBENV_RUBY_PATH/bin"
PATH="$PATH:$HOME/.rbenv/libexec"
PATH="$PATH:$HOME/.rbenv/plugins/ruby-build/bin"
PATH="$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH
export RBENV_VERSION
export GEM_HOME
export RUBYLIB="$_BUNDLER_PATH/lib:$HOME/.rbenv/rbenv.d/exec/gem-rehash"
export BUNDLE_BIN_PATH="$_BUNDLER_PATH/bin/bundle"
export MANPATH=''

# Execute Unicorn.
exec $HOME/.rbenv/shims/bundle exec \
    --keep-file-descriptors unicorn -E production \
    -c $MY_APP/config/unicorn.conf.rb

When Unicorn receives a USR2 signal, it will re-execute itself. You can tell Unicorn to execute a different file instead, such as our shim. Insert this line somewhere near the start of your Unicorn configuration file:

Unicorn::HttpServer::START_CTX[0] = "/usr/local/libexec/unicorn"

Your application is now ready to migrate to a new version of Ruby without any downtime at all. Simply send the appropriate signals to the Unicorn master as per the zero-downtime method. The shim will handle any Ruby version changes.

What version of Ruby is Unicorn using?

After restarting Unicorn, there are two ways to verify that Unicorn is using the correct version of Ruby.

Use lsof to show what files the Unicorn master process has open. Replace PID with the process identifier of the Unicorn master process.

# lsof -p PID -Fn
n/home/discourse/.rbenv/versions/2.2.2/bin/ruby
...

You can also view the environment variables for the Unicorn master process to check that they've been set correctly:

# xargs -a /proc/PID/environ -0 printf '%s\n'
RBENV_VERSION=2.2.2
GEM_HOME=/var/www/discourse/vendor/bundle/ruby/2.2.0
...