March 30, 2015

Moving Contactually to Rails 4.2

Title slide bg wide

A brief history of Contactually's usage of Rails

When we first started building Contactually three years ago, we were running Rails 3.2.8 and felt pretty proud to be on the bleeding edge. Through the years, we dutifully upgraded through all of the subsequent point releases, but never made the major version jump to 4.x.

Increasingly, the gems we use to run the project stopped supporting 3.x, and we soon realized it was time to get off that sinking ship while we still could. We didn't want to fall too far behind the major versions of Rails, or our most heavily-used gems. We were losing out on potential performance gains, new features, and community support -- and truthfully, we just like playing with the latest and greatest. Most importantly though, the biggest reason for us to upgrade was simply to stay at least one major version away from obsolescence.

For those of you who still need to move up to Rails 4.2, here's what that transition looks like, circa February 2015.

The process

We dipped our toes in the water by first updating the required version of Rails in our Gemfile. While we were at it, we also upgraded the Mongoid version from 3.1.3 to 4.0.0. After that, it was a simple bundle install to upgrade our Gemfile.lock.

Our old configuration files needed to be updated before we could even see how many of our tests were failing, so we started a new, empty Rails 4 project to compare them. The first thing we noticed was that the blank project generally used Rails.application and the only place OUR_APP_NAME was referenced was module OUR_APP_NAME in config/application.rb. However, throughout our environment and files we were using OUR_APP_NAME::Application, so we changed OUR_APP_NAME::Application to Rails.application to be consistent.

There were also a bunch of configuration options that moved from config/application.rb to their own initializers, or were simply removed altogether. We were using config.whiny_nils, config.best_standards_support, and config.assets.compress , all of which were deprecated in Rails 4.2. Asset related configs are now located in config/initializers/assets.rb and parameter filtering has moved to config/initializers/filter_parameter_logging.rb.

Our routes defined by the match method needed to be updated to either:

  • explicitly use the http verb, or

  • add the via option to list which verbs should match this route.

Since 4.0, Rails has used PATCH as the primary verb for the update action when using resource, but as it still creates a PUT route, we were okay there.

Another concern was keeping out users logged in across our upgrade. Fortunately, the Rails developers have made transitioning to more secure cookies pretty painless.

  • In Rails 4, cookies are encrypted, whereas in Rails 3 they're merely signed (although the Rails developers anticipated this). By leaving in our secret_token, Rails 4 can read the old cookies, and then use the session_store key to encrypt them.

  • Prior to Rails 4.1, cookies were serialized via Marshal, but from Rails 4.1 on the default is JSON. Thankfully, setting the serializer to :hybridwill handle unserializing a Marshalled cookie, and then serialize things into :json. Marshal is a bit faster, but also more dangerous.

Upgrading Mongoid also brought some major changes that weren't always backwards compatible. First, the configuration option for allowing dynamic fields moved from actually being a configuration option to being a module included in each model as needed with include Mongoid::Attributes::Dynamic.

Next, our default scopes had to change to Procs -- we had to monkey patch BSON::ObjectId for our Mongoid ID serialization.

[box type="shadow"]
module BSON
class ObjectId
def as_json(options = {})


Rails 4 changed the has_many handles :conditions, :order, :uniq, ... well, really any option that changes the SQL query for the assocication. Now those strings options are Procs, using the actual ActiveRecord::QueryMethods methods. So lines like:

[box type="shadow"]
has_many :buckets, :order => "LOWER('name') ASC"


become more like:

[box type="shadow"]
has_many :buckets, -> { order("LOWER('name') ASC") }


Rails' view helpers :confirm and :disable_with options had to be moved inside a hash with a key of :data. So...
=submit_tag 'Delete', :confirm => "Are you absolutely sure?", :disable_with => "Deleting..."

... becomes :
=submit_tag 'Delete', :data => { :confirm => "Are you absolutely sure?", :disable_with => "Deleting..." }

Another issue was strong_params. As we started to make the required methods in our various controllers, we ended up with a decent amount of duplication. We DRY'ed those up by putting them in separate ActiveSupport::Concern modules, and included the ones required for each controller.

There, that wasn't so bad

We then went through our usual QA process, which includes deploying to our staging server. Doing so revealed our asset pipeline wasn't configured correctly for our static assets. config.serve_static_files had to be changed from true to ENV['RAILS_SERVE_STATIC_FILES'].present? -- which is false for production and staging (since we use a CDN to serve static assets), but true for our local development environments. There were a few other odds and ends which really speaks more to the importance of thorough testing, as these problems can pop up even with point releases. Overall, as far as Rails upgrades go, this wasn't as badas it could have been thanks to the clean upgrade paths provided by the Rails developers. Despite so many moving pieces, it was still a lot easier than previous major upgrades.

Now we're set to do it all over again with Rails 5 in the fall.