How to migrate monolith to the scary new version of Rails
Tags: ruby, rails, upgrade
Published: 2018-09-25 19:14:08

---
title: How to migrate monolith to the scary new version of Rails
published: true
description: Common techniques and tips for doing painless incremental Rails upgrade
tags: #ruby #rails #upgrade
---

Migration to the new version of Rails always looks scary. Thinking about the upgrade raises many questions, especially if your application is an older monolith. The most critical points are:

- upgrading without blocking feature development
- affecting as fewer users as possible
- making sure all logic works correctly on the new version of Rails

In this article, we will review some common techniques and tips which let Amplifr’s team have the painless incremental upgrade to the latest version of Rails.

❗The main tip - is to **upgrade incrementally only to the [latest patch version](https://rubygems.org/gems/rails/versions) of Rails at once**, because most of the bugs have already been solved. And this choice minimises breaking changes you need to deal with each upgrade.

We had done three upgrades steps: `4.2.0 => 5.0.7` , `5.0.7 => 5.1.6`, `5.1.6 => 5.2.0`.

Here are the standard steps of one Rails version upgrade cycle and delivering it to production:

1. **deprecations:** clean up all deprecations, which block the next Rails version update
2. **dual boot:** make application runnable with current and next version of Rails
3. **green tests:** make tests pass for both Rails (matrix build)
4. **deploy → monitor → (revert) → fix:** switch to the new version of Rails, monitor application for the new failures and bugs, revert if bugs are critical and you need time to fix them
5. **keep doing** point number four until everything is ok

Repeat until you are on the top Rails version 😁 

# Deprecations

Deprecations say that something can break when a third party dependency gets updated. As we are going to upgrade Rails - we have to fix the blockers for the next version of Rails.

The main problem is to separate the target version blockers from fixing them first. Hiding the future version deprecations by using this dangerous and ugly-hack worked for us:

```ruby
    if Rails::VERSION::MAJOR == 5
      # temporary mute test params deprecations
      ActiveSupport::Deprecation.behavior = lambda do |msg, stack|
        unless msg =~ /Using positional arguments in functional tests has been deprecated/
          ActiveSupport::Deprecation::DEFAULT_BEHAVIORS[:stderr].call(msg, stack)
        end
      end
    end
```

❗**It is better to fix all deprecations at once** if it’s possible, it will save a lot of time in the future


# Dual boot

The idea is to support both versions of Rails at the same time. Keeping application runnable on current Rails version guarantees that you will be able to run the application on it if something goes wrong. Making application runnable on the next Rails version will allow you to have continuous upgrade.

Make your application able to run with both versions of Rails depending on environment variable `RAILS_NEXT`:
This command should boot current Rails version

```bash
    bundle exec rails c
```

And this will boot application with the new version of Rails:

```bash
    RAILS_NEXT=1 bundle exec rails c
```

The way to do it - is to hack `bundler` a little bit by putting this code at the beginning of `Gemfile`:

```ruby
module Bundler::SharedHelpers
  def default_lockfile=(path)
    @default_lockfile = path
  end
    
  def default_lockfile
    @default_lockfile ||= Pathname.new("#{default_gemfile}.lock")
  end
end
    
module ::Kernel
  def rails_next?
    ENV["RAILS_NEXT"] == '1'
  end
end
    
if rails_next?
  Bundler::SharedHelpers.default_lockfile =
    Pathname.new("#{Bundler::SharedHelpers.default_gemfile}_next.lock")
    
  class Bundler::Dsl
    unless method_defined?(:to_definition_unpatched)
      alias_method :to_definition_unpatched, 
                   :to_definition
    end
    
    def to_definition(_bad_lockfile, unlock)
      to_definition_unpatched(Bundler::SharedHelpers.default_lockfile, unlock)
    end
  end
end
    
gem 'rails' #relax dependency
```

This code does a couple of things:

- Defines the `rails_next`? method in `Kernel` class, so this method is available to use everywhere: in `Gemfile`, in your ruby application’s code and so on
- Defines singleton-method to hold the current bundler’s lock-file name
- Reset the lock-file name to `Gemfile_next.lock` if `rails_next?` is activated

❗Note, that we **use one `Gemfile` and two lock-files for every version of Rails**. It is much easier to maintain.


# Dependencies

First of all, relax Rails dependencies in `Gemfile` to enable higher versions of Rails:
If you have rails version locked like this:

```ruby
gem 'rails', '~> 4.2.3'
```

Unlock it. 

```ruby
gem 'rails'
```

It allows `Gemfile` to be compatible with all versions of Rails. Current and next rails version will be locked in the corresponding `lock` -files.

Then copy `Gemfile.lock` to `Gemfile_next.lock` and try to update rails on `rails_next`:

```bash
    $ cp Gemfile.lock Gemfile_next.lock
    # run bundle for fixing dependencies matedata in original Gemfile
    $ bundle
    # run bundle on rails_next to update rails
    $ RAILS_NEXT=1 bundle update rails
```

Here you will get bundler working hard to resolve dependencies and probably fail in the middle. Failure means that some of the dependencies are not allowed to work with the newer version of Rails.
Here are some scenarios:

- Gem has current version locked and not supported. And it has the newer version to support the newer version of Rails. We have to update gem if only the `rails_next?` works:

```ruby
    if rails_next?
      gem 'devise'
    else
      gem 'devise', git: 'https://github.com/plataformatec/devise', branch: '3-stable'
    end
```

- Gem has Rails version locked from above: `rails < 5.0` but probably works with newer version. You need to unlock dependencies and give it a try.
  Try to clone this gem from GitHub locally, relax dependency of Rails in `gemspec` file and plug gem locally. If it works - great, move to the next blocker, if not - try other solutions.
- Gem has Rails locked, because of incompatibility and does not work with newer version. You need to unlock dependencies, understand how gem works and where is the problem, then fix all the conflicts and enable tests for the new version of rails in this gem.

Dealing with lasts cases - you will have to update gem source code. First of all, you will fix the problem in your fork. Despite this way looks very simple, fast and easy - there are many reasons for avoiding keeping it in forks:

- Authors (or a group of authors) maintain their gems in a centralised way. Nobody is interested in supporting your fork 😢  Your fork is your problem!
- Forking let you fix the problem fast, without design work and tests, but it is just postponement of the problem. You will pay back when upgrading the fork from the remote source in future
- Fix a problem in a centralised way (by sending pull-request) helps other developers to deal with the same problem. Feel free to contribute by submitting the pull-request!

❗The better way to fix gem’s source code - is to **send pull-request to the gem’s repository**. 

Unfortunately, some gems do not have anybody to maintain because of many reasons. And this is the signal for you to inspect your dependencies and get rid of unmaintainable ones in favour of more popular.

❗Note that you have to **update all gems twice to support two version of Rails**, and commit both `Gemfile.lock` and `Gemfile_next.lock` to keep them similar.

```bash
    bundle update some-gem
    RAILS_NEXT=1 bundle update some-gem
```

# Runnable app

When all dependencies are okay, and your bundler successfully resolved for the `rails_next` - you are ready to proceed to the next step - check if the application is runnable.

You could try to run Rails console or local web-server, but it would be better to start from running the tests:

```bash
    RAILS_NEXT=1 bundle exec rspec
```

With a high probability tests would not start or even fail. The main problem here is the incompatibilities of your code or some gems’ code with new Rails version. So only one tip: go deeper and fix it.

❗ One trick we used - is to write **Rails version dependent code using `rails_next?` method temporarily**:
```ruby
    project.run_callbacks(:commit) if rails_next?
```
It helps to write the version-specific code:
```ruby
    if rails_next?
      redirect_to user_omniauth_authorize_path(:instagram)
    else
      redirect_to user_instagram_omniauth_authorize_path
    end
```

# Green tests

Add the extra build to you CI for running tests with the next Rails version. If you are running on TravisCI, it might look like this:
```yaml
    matrix:
      include:
        - env: RAILS_NEXT=0
        - env: RAILS_NEXT=1
```
If your next Rails version tests not pass yet - you can mark them optional like this:
```yaml
    matrix:
      include:
        - env: RAILS_NEXT=0
        - env: RAILS_NEXT=1
      allow_failures:
        - env: RAILS_NEXT=1
```
This approach will let you merge matrix-build to the main branch without affecting feature-development, and you’ll be able to work on tests in parallel.

Finally, you need to make your tests green for both versions.

❗**Getting upgrade without having tests - is not a very good idea** 🙂, because it’s impossible to ensure crucial application parts work on the next version.

# Deploy & Monitor

It time to rollout!

If you have staging - deploy there first and check the main functionality by hands. It minimises risks and may give a chance to catch some bugs.

Running in production is more effective. It makes the real users test application within the reals cases. The main idea is to deploy the new Rails version by “a little bit” and keeping in mind revert strategy, that would help you not to affect users too much or too long.

Depending on your infrastructure you may prefer to use one of the rollout strategies, or combine them:

1. serving specific endpoints with new Rails version application instance, starting from low-loaded to high-loaded, or starting from non-critical.
2. serving the percentage of overall load, and then run to increase the rate.
3. rollout the application at the low-loaded time (if you have those), e.g. at night-time
4. rollout 100% and keep calm 🙂 

We decided to roll out the all Amplifr with new Rails version, because it is the most progressive way, and rolling back to the previous Rails version took a couple of minutes for us.

Once you’ve rolled out the application (or it’s part) and let real users face it - it is essential to keep monitor errors.
There are two general ways to get to know about problems:

1. automatic monitor with some tools like `Honeybadger`, `Rollbar`, `Sentry`, `NewRelic`. **Having it - is a must!** They are useful to be notified about causes of issues and bugs online with Slack, email or other ways
2. real users are critically helpful for small teams like Amplifr. No matter how much you’ve tested the application and how high test-coverage you have – it’s likely that you missed few bugs, especially in business logic.  Always be kind and patient with your users, and they will behave the same way when you need their help.🙂

If something goes wrong - revert by changing the value of `RAILS_NEXT`  environment variable. The exact way depends on the infrastructure you have:
```bash
    export RAILS_NEXT=0
```

❗**Rolling back does not mean failure**, so do not worry. It gives you the time to fix newly found errors while the application runs stable on the previous version of Rails. **Just keep it up!**

We have several rollbacks for fixing the critical bugs in Amplifr.

❗After the successful upgrade, let the application to work on the new version of Rails for a long-term to minimise risks and fix all the bugs.


## Cleaning up

After the successful upgrade, we have clean up the source code and cut off the support of the previous Rails.

- Make the application run with the new version of Rails by default. You have to copy next `lock`-file content and commit it:
    cat Gemfile_next.lock > Gemfile.lock
    git commit ...
- Deploy the application and switch RAILS_NEXT `1` → `0`
- Cut off the `Gemfile_next.lock`:
      git rm Gemfile_next.lock
      git commit
- Remove all the version dependent code; you can easily find it:
    grep -ir "rails_next\?" ./
- Remove `Bunder`'s hacks from the `Gemfile`

We haven’t removed the dual boot code and kept running on the non-updated `lock`-file for a couple of days. 🤦‍♂️ Be careful with it. 🙂 

  

❗If you are going to keep upgrading - run from the begging with the next version.


## Stay on edge

If you are on the last stable version, you might prefer to stop on it. Another option is to keep upgrading with the `master` branch as next version of Rails.

```ruby
    gem 'rails', git: 'git@github.com/rails/rails'
```

This way is not single-valued but probably is the progressive one. Most of the gems do not support the newer version, and you will have to contribute there. It could be dangerous and make you catch bugs of the unstable features in Rails code.
But from the other hand - you will keep on the wave with the new version of Rails could make your upgrade painless and comfortable in the future, and be helpful by contributing to Ruby’s gems source codes.


# Conclusion

Although upgrade Rails version might look like a tremendous amount of work, it is essential to be on the latest version, because of this reasons:

- it is more stable, fast
- it has new features
- it lets you use the latest versions of gem’s

Doing upgrades iteratively, step by step, and keeping in mind revert strategy can make your upgrade less painful.

# Further reading(watching):
 - [Upgrading a Rails application incrementally](http://www.recursion.org/incremental-rails-upgrade/) by Luke Francl
 - RailsConf 2017: [Upgrading a big application to Rails 5](https://www.youtube.com/watch?v=I-2Xy3RS1ns) by Rafael França
 - [Upgrading Shopify to Rails 5](https://engineering.shopify.com/blogs/engineering/upgrading-shopify-to-rails-5-0) at the Shopify's Blog
 - [Shopify's Deprecation Toolkit use cases](https://engineering.shopify.com/blogs/engineering/introducing-the-deprecation-toolkit) at the Shopify's Blog
 - [Ten Years of Rails Upgrades](https://speakerdeck.com/jnraine/ten-years-of-rails-upgrades) slides by Jordan Raine
 - [How we upgraded a big Ruby on Rails monolith with Zero Downtime](https://bytes.babbel.com/en/articles/2015-12-14-rails-4-upgrade.html)
 - [Kir Shatrov's talk about Rails upgrade at Shopify (russian language)](https://www.youtube.com/watch?v=nqGxzUGtkNw)