27 Feb 2015

Upgrading a large Rails 3 app to Rails 4

Last month I worked on upgrading GOV.UK’s biggest application (in terms of code, users and volume of content) from Rails 3 to Rails 4. Here’s the monster pull request. I wish I could say it was a straightforward undertaking. In the end, it involved one Rails patch, patches to three other open source libraries, and a lot of sleuthing and debugging.

There’s a lot you can do to reduce the risk and the pain of the transition from Rails 3 to 4. The TL;DR? Instead of one big-bang upgrade to Rails 4, try and split the process into lots of mini-upgrades. Here’s what I recommend:

1. Understand the major changes

The Guide for Upgrading Ruby on Rails is a good place to start. It provides a good overview of the major changes between versions.

2. Backport as much as you can to the current version of your app

A lot of the changes you’ll make for the Rails 4 upgrade will be backwards-compatible with the Rails 3 version of your app, making it possible to backport a lot of it. This will make the final upgrade to Rails 4 smaller, simpler and safer. Once you have a working Rails 4 branch, go through commits and see which ones can be cherry-picked back onto the Rails 3 version. This is where having well-formed atomic commits will make your life much easier.

3. Move to strong-parameters

Rails 4 deprecates both attr_accessible and attr_protected for mass-assignment protection. To help ease the transition to Rails 4, I recommend moving to the strong-parameters gem on the Rails 3 version of your app before performing the Rails 4 upgrade. In my experience, the move to strong-parameters can be a tricky one and doing it separately will make dealing with any issues much easier.

4. Audit your Gemfile

Upgrading from Rails 3 to 4 will almost certainly require some major updates to your Gemfile as many libraries tend to break compatibility between major versions. As above, you can save yourself a lot of headache by updating as many gems as you can on the Rails 3 version of your app before the switch to Rails 4.

In our case, we went a step further and performed a complete gem audit, looking at all the outdated gems to figure out:

  • Is there a newer version of the gem that is compatible with both Rails 3 and 4?
  • How big are the changes between the current and newer version?
  • How much risk is associated with updating the gem?

Armed with this data, we were able to update, test and roll out a whole bunch of gem updates on the existing Rails 3 code in a planned and controlled manner. This gave us a chance to catch and deal with any compatibility issues early and reduce the number of risky gem updates in the final upgrade to Rails 4.

5. Don’t jump straight to the latest version of Rails

Going from Rails 3.2 to Rails 4.0 will be an order of magnitude easier than jumping straight to 4.2, or whatever the latest version is – you’ll get the benefit of a bunch of deprecation warnings, and thus fewer breaking changes. The subsequent upgrades from 4.0 to 4.1 and then to 4.2 should be simpler and less risky as a result. That said, once you’ve made it to Rails 4.0, don’t delay in getting to Rails 4.1 or later as 4.0 is now end-of-life and will no longer be receiving security updates!

6. Good test coverage helps a lot, but poorly-formed tests are a pain

It’s times like this when a comprehensive test suite is super helpful. Having said that, some tests end up causing as much pain as they save. I’ve found this particularly the case with tests that make excessive and unnecessary use of stubs and mocks. A significant number of the commits in this upgrade were solely rewriting tests that had broken, not because behaviour had changed, but because the existing tests were not robust enough and were often tightly coupled to the internal implementation of the methods under test. Lesson here: avoid writing brittle, heavily stubbed tests!

7. Start at the lowest level and reduce the noise first

My focus was to get the unit tests passing first. Although this was the bulk of the work, it helped to make the whole process a little less overwhelming, and once the unit tests were green, getting the rest of the test suite to pass was much easier. Getting rid of the deprecation warnings as an early step also helped to build some momentum and reduce the amount of noise in the test output.

8. Avoid non-reversible migrations and session changes

Even with thorough testing, QA and preparation, there is always a small risk that you will need to roll back your Rails 4 code. Make this as easy as possible for yourself by avoiding changes that cannot be easily rolled back. The main things to avoid are:

The latter is particularly important, as user sessions that have been upgraded to the new signing code cannot be reversed. This means that if you have to roll back to Rails 3, you’ll break a bunch of user sessions. I would recommend not setting the new secret_key_base config attribute with the upgrade and living with the deprecation warnings until you are confident that your Rails 4 code is stable and won’t be rolled back. You can then upgrade to the newer session handling code as a separate piece of work.

12 Sep 2014

Performance testing Rails against real data

Have you ever had painfully slow requests in your Rails application? Tools like NewRelic and rack-mini-profiler are great for identifying the cause of poor performance. You can then use performance tests to write repeatable benchmarks and use them to measure and evaluate your optimisations. But out of the box, these tests won’t always give you the entire picture: often requests will only perform poorly when made against a full production database. In such cases, standard Rails performance tests give you a false measure, making it harder to test the benefit of your optimisations.

The good news is that it’s fairly straightforward to run your performance tests against real-world data. Here’s how:

1. Configure a new environment for your “real-world” benchmarks in database.yml

Here I’m pointing the config at my development database because it already has a close approximation of production data in it. Feel free to set up an entirely separate database for your benchmarks.

2. Add a config/environments/benchmark.rb file:

Instead of using the existing test.rb configuration, we create a new one that more closely mirrors production.rb. This allows us to do things like turn caching on. This is particularly useful when your optimisations rely on using caching. Note that this should be tuned to the specifics of you application.

3. Create a test/benchmark_helper.rb file to setup your benchmark tests:

You can add any other dependencies your benchmark tests rely on in here, as well as configure the default profiling options as you see fit.

4. Write a performance test:

This requires benchmark_helper.rb so that it runs against the correct environment. These tests look like standard rails integration tests. Note that you’ll have access to a full database, so you can load any specific instances you need right there in the tests; no need for fixtures or factories.

5. Add a rake task for running your real-world benchmarks:

This rake task will run all the tests in test/performance against your benchmark database. Note that the main difference between this and the default test:benchmark rake task is that it skips the test:prepare step, which would rebuild your database, clearing it of all data. That would be bad.

6. Run your benchmark tests with the newly created rake task:

$ bundle exec rake test:real_world_benchmark

This generates outputs for each test in the tmp/performance directory. You can now run these tests both before and after your optimisations to see exactly how much of an improvement you’ve made. Job done!

Note: These instructions are for Rails 3 applications. The performance test functionality was extracted out of Rails 4 to a separate gem. It should be possible to get real-world benchmarks working on a Rails 4 application by including this gem in your Gemfile and making any tweaks as necessary.

View older posts »