2017 Apr 08

Decent coverage is your friend here

There's no secret here: we love Rails. The ability to get an application up and running quickly, using well established conventions and design patterns, as well as tried-and-true web tooling is extremely valuable when you're building web applications all day, every day.

We like it so much, that we built the Wicket API in Rails. At the time of product conception, Rails 4.2 was the latest version with 5.0 in beta/RC throughout. A few months after the production launch of Wicket, Rails 5.0 was released.

Why upgrade?

While 4.2 met (and continues) to meet our current needs for Wicket, some of the new features in Rails 5 are very compelling. Most notably, Action Cable which will allow us to implement a WebSocket layer in Wicket for real-time interface updating.

Bringing the API mode directly into Rails core, supporting later versions of Ruby and supporting the handy new .or in ActiveRecord queries are all just icing on the cake. There's a lot of heavy lifting under-the-hood across all of the various Rails dependencies that would take hours to wade through.

We like modern tech at Industrial, and this means keeping on top of the latest changes to the tools in our toolshed and upgrading once newer versions are released. Rails 5.1 is on the horizon, so delaying this upgrade is only going to make our work more challenging in the future.

So, let's dive in!

How?

Early on, we spent a few hours poking at 5 to see what it would take to move from 4.2 to 5. We studiously read The Guide, which was helpful, but in a real world application there is a lot more nuance that a standard upgrade guide will miss. Without maintaining that initial attempt to move to 5, months went by and code merges became so problematic that we decided to start from scratch. Here's a walkthrough of what we did to upgrade a production Rails 4 app to 5.

The upgrade guide (linked above) contains a number of items to watch out for when upgrading. In our case, running Rails 4 through the rails-api gem (which was merged into Rails 5) meant that not all of the upgrade notes were applicable to our installation as we were already leveraging some of the work in that project.

Here are some of the highlights that were relevant to us, with associated fixes:

  • Ruby 2.2.2+ required
    • Easy for us, we were already using 2.2.3 through rbenv (which is awesome).
  • Active Record Models now inherit from ApplicationRecord by default
    • Similarly to most Rails apps, we already had a Base Active Record model. We initially altered it to inherit from ApplicationRecord, but ran through all of of our model definitions to use it instead. Future migrations that create models will use it, and we always ran into issues where new models didn't always get updated correctly to inherit from Base.
    • An additional issue we ran into here was that we have a number of models that define a type that are not polymorphic. Ensuring that ApplicationRecord had a self.inheritance_column = nil ensured that these models continued to work as intended.
  • Halting Callback Chains via throw(:abort)
    • This is a pretty big change if you depend on before_ callbacks to halt execution. While we don't prescribe to the "escape from callback hell" mentality, we do believe in using them as sparingly as possible and did not have any instances of using before_ in this way.
  • Extraction of some helper methods to rails-controller-testing
    • Our tests use assigns a lot. Easy to solve by including the rails-controller-testing gem in the test block of your Gemfile. We'll likely move away from this usage in the future so that we can drop this gem dependency.
  • Use bin/rails for running tasks and tests
    • Surprisingly difficult to do! When you've been accustomed to rake tasks for years, the use of rails as a command runner seems odd. However, if you stick to rake, it can (and did) cause some unpredictable results when running tests so we highly recommend getting comfortable here.
  • ActionController::Parameters no longer inherits from HashWithIndifferentAccess
    • This is a big one. Given that it's an API we're developing, we leverage strong parameters heavily. We use JSONAPI in Wicket, and a lot of logic walked through the params passed into action methods expected hashes. These were re-worked to use the object references instead. Thankfully, these methods were all in ApplicationController, so it was a relatively easy process to update (DRY FTW).
  • ActiveSupport::TestCase Default Test Order is now Random
    • This is both a boon and a benefit (definitely more of the latter). We didn't realize some of the potential faults in our tests until they were randomized. For example, we use sidekiq for our background job processing, and had missed the super on the recommended after_teardown callback which caused random test passes to fail.

Hanging tests

For that last item above, we really struggled in tracking down the cause of the failing tests as the process would just hang.

To work around this, we added the following to the bottom of test_helper.rb (we're using MiniTest) and if you identify the PID of the running rails test process (ps -ef | grep ruby) you can issue a kill -USR1 <PID> which will dump a stack trace. Running our tests with a consistent seed (rails test --seed <NUMBER>) and getting the stack trace narrowed down the files causing the issue which we were able to resolve.

puts "rspec pid: #{Process.pid}"

trap 'USR1' do
  threads = Thread.list

  puts
  puts "=" * 80
  puts "Received USR1 signal; printing all #{threads.count} thread backtraces."

  threads.each do |thr|
    description = thr == Thread.main ? "Main thread" : thr.inspect
    puts
    puts "#{description} backtrace: "
    puts thr.backtrace.join("\n")
  end

  puts "=" * 80
end

Other useful notes

While the above items were the most obvious from the guide, here's a few other recommendations based on our experience.

  • Indiscriminate "bundle update" will not be successful. Depending on the number of gems you use, a more judicious process of updating specific gems, reviewing the conflict notes, and repeating will get you to the least operable gem set first. The key is watching the greater than / less than versions in the bundler messages that are output and running bundle update <GEM> individually. Once you've established a gem set that passes, start upgrading! Take advantage of some of the latter improvements to such key awesome gems such as puma, paperclip and i18n.
  • Do not forget the rails app:update command after running bundle update rails (this updates Rails and it's gem dependencies). This is important, as it introduces a number of sensible defaults across the environment files, bin files such as rails and rake, and adds / alters some new initializers. Be careful to watch changes between the new templated versions, and any changes that you may have made in your existing application.
  • When in doubt, stop spring. If you're swapping between Rails versions, spring can cause some unintended effects if there is a version support issue between your application and the running version. spring stop is your friend here. It will be restarted automatically through the other commands.
  • Watch those deprecation notices. There are a lot of deprecations added in Rails 5.0 that are planned for removal in 5.1. The biggest change for us was the switch to keyword parameters in our controller tests for the request methods. Relatively easy to fix, just a bit of grunt work to walk through all of the controller tests.
  • Another big change is the new renderer. We relied on ApplicationController for any actions that invoked render or render_to_string (e.g. generating axlsx files in Sidekiq workers). The default for Rails API is to not have the render methods available in ApplicationController, so we recommend switching over to using ActionController::Base instead (ActionController::Base.new.render_to_string).
  • We had a lot of chained uniq methods. In Rails 5.1, this will be deprecated in favour of distinct.
  • Merge often. If you're deep in feature development, there will likely be many branches being updated frequently. We abandoned our first upgrade attempt due to poor maintenance of the work done. Keep it up if you're planning a major upgrade in the near future.

Next

We're not entirely out of the woods yet with the upgrade, but we have a full test suite passing and we're about to move Wicket into our QA process to look for regressions.

The first step is ensuring that you have decent test coverage on your application. Without this, upgrading from Rails 4 to 5 will be an uphill battle that will require a lot of manual testing and QA. We recommend that you take the opportunity to either start or step up your test coverage before attempting this process, or it will be far more difficult.

We're excited about many of the upcoming changes to Wicket, but ensuring that we're using the best available versions of the technologies we depend upon is key. Getting these changes through our internal QA process is next up to ensure that there is no regression that our tests miss, and then it's onto production.

Hopefully, some of the lessons we've learned in our upgrade process will help others in upgrading their applications.