Last week I have upgraded our Dockbit application to Rails 5. The codebase is rather small, so it’s been a relatively painless process. In this post, I would like to summarize the practices that I follow when upgrading to major Rails releases, which have been pretty common for me since Rails 2. I also wanted to gather all the things I had to consider in one place, hence this blog post.
Every application is different, so is the upgrade process. However, I am quite sure you would find some similarities and valuable information here that would help you with your Rails apps upgrade, so keep reading.
1. Deciding to upgrade
My decision to upgrade to a new version of Rails usually comes from finding one or two awesome features that I’d really need. Frankly, I don’t do it for the sake of running the latest & greatest. But on the other hand, I try not to procrastinate either. Every software upgrade comes at a cost, but delaying for long, costs even more.
For us, the main theme was the ActionCable. I was excited about it ever since I first heard DHH introducing it at the RailsConf 2015. Totally agree with David, dealing with WebSockets is a pain in the butt and I wanted something that would seamlessly integrate with our Rails app, following the familiar conventions and without the need to run anything on the side — no more polling, no Server-Sent Events — yay!
If you haven’t watched the talk, check it out here:
As we really needed real-time features to stream the logs and statuses of Dockbit powered deployments, we actually brought ActionCable to Rails 4 and have been running it for a couple of months in production. ActionCable used to be a separate repository in GitHub and then it was merged to Rails core. Luckily, I had forked it right away so we could continue using it.
The biggest question for us was how to pass the data from the server to the client. We’ve got a pretty standard Rails app with server-side rendered templates, so sending over JSON and building a new view layer on the client would have required a massive rewrite. It’s when I found out there is this “hidden gem” in Rails 5 that lets you render templates outside controllers — exactly what I was looking for to send our updates to the client:
That was the “aha — I have to upgrade to Rails 5” moment for me 😛
2. Studying what’s new and changed
Perhaps the most challenging part of upgrading to any major software release is understanding how much work it would require. I usually start with studying what’s new and more importantly what has changed. By the way, there are lots of good posts covering the new features of Rails 5, so I will just mention one that I really liked: “What’s new in Rails 5?” by Mario Alberto Chavez, though I am sure there are more great posts that I haven’t yet discovered.
You might be excited about the new features, get all the Gem dependencies right and then find out that some core Rails changes break your app. I believe it’s crucial to study the pre-release notes (a.k.a. CHANGELOG), or if one doesn’t exist yet, skim through the GitHub commits.
What I really love about Rails and Ruby community in general, is the attention to documentation. It might be tedious, but reading through the commit messages would already give you a good overview of what might break your app and needs digging further (source code, asking people, etc.).
2. Preparing to upgrade
Importance of testing
After having studied the new Rails features and possible breaking changes, in most cases you would have a pretty good understanding of the areas requiring your attention. That’s a good time to check your test coverage and make sure the critical parts of your application are well tested.
Fighting gem dependencies
The first thing to do before starting any upgrade is to check your Ruby Gem dependencies. If you aren’t Basecamp, you probably use a bunch of gems that extend the functionality of your app beyond the Rails’s “default backpack”. And frankly, this is the most annoying part of any upgrade process— fighting with software dependencies.
It’s that time when you find the library you’ve been using is no longer maintained, or when you start feeling “unsafe” by linking Git repositories directly in your Gemfile.
Bundler does a good job figuring out the versions you need to satisfy your gem dependencies. In most cases, though, you would need to reference the GitHub’s master or rails5 branches. I hope the latter is becoming a convention in the community, which is really a neat way to let developers know whether the particular library works towards the next major Rails release.
If you found that particular gem is no longer maintained or you don’t want to wait for it to be “ready”, you would need to start looking for a replacement or writing your own implementation. This reminded me of an excellent post by ThoughtBot’s Elle Meredith — To gem or not to gem.
In any case, be sure to check the CHANGELOGs and commit histories of the gems you’re using. There might be more breaking changes that will add up the time to your upgrade process.
Another important decision to make is how and when to take the time for the upgrade. Without going into too many details, which are anyway team/app specific, what worked well for the teams I worked with is doing it on the side, as opposed to one-shot migration.
For bigger apps, it’s less risky and your estimation of “hey, it will just take me a couple of days” is most probably wrong. By the way, is that what we always tell before every major software rewrite? 😉 We simply can’t be aware of all the pitfalls, which is one of the things that makes me love the nature of software development —isn’t it adventurous?!
If you found the upgrade process would take much longer, it sometimes makes sense to invest in making your app “cluster” ready, meaning that you can run multiple versions of the app simultaneously. This would allow you to use and test the new version internally, even before it’s shipped to your users.
4. The actual upgrade
Alright, assuming you have all the gem dependencies satisfied, we’re ready to get to the actual upgrade. I’ve prepared a list of things I had to do to make our application work with Rails 5.
Perhaps the best way to understand Rails environment configuration changes is to generate a Rails project with the new release and compare/integrate the boilerplate code to your current app. This way you’ll learn what’s changed and the new defaults. In most cases you would want to stick to the new configuration, but these are the things that worth your attention:
- Initially, I’ve disabled “belongs_to required by default” feature (see “Bringing in new features” later in this post)
# config/initializers/active_record_belongs_to_required_by_default.rbRails.application.config.active_record.belongs_to_required_by_default = false
2. I’ve enabled back ActiveRecord’s “halting callback chains” (see “Bringing in new features” later in this post)
# config/initializers/callback_terminator.rbActiveSupport.halt_callback_chains_on_return_false = true
3. I’ve also fixed autoloading of classes from the lib directory, so instead of autoload_paths, used eager_load_paths. This one I only found in production mode, which was also mentioned in Don’t forget about eager_load when extending autoload paths post by Robert Pankowecki.
Breaking changes and deprecations
Below is the list of things that I had to fix in order to get back to a working application, passing tests and no deprecation warnings. Your list might be longer and really depends on what Rails features and gems you are using.
- Sprockets 4 manifest
I had to create the app/assets/config/manifest.js to load all the assets properly. I’ve also listed fonts in it and removed them from the config/initializers/assets.rb. Read more about it in Basecamp’s Eileen M. Uchitelle blog post.
2. ActionController::Parameters return object instead of hash
This one is quite important to understand and might bring a lot of problems if you’re fetching parameters outside of StrongParameters’s require key. Again, Basecamp’s Eileen M. Uchitelle blog post helped a lot, so I had to use a mix of to_h, to_unsafe_h and Hash#merge.
3. Nil values in link_to and image_url helpers
If you’re having nil values in link_to or image_url helpers, it would break as Rails tries to generate the URL for it. I ended up just replacing them with plain HTML link tags.
4. Keyword arguments and XHR requests in functional tests
Functional tests are now using Ruby’s keyword arguments to pass parameters, where xhr key is one of them. Follow deprecation warnings with respective examples in the Rails console to help you fix those.
5. assigns and assert_template have been extracted to rails-controller-testing, so either remove them or bring in the gem.
6. render nothing: true has been deprecated, so just change that to head with the HTTP status code appropriate for your case.
7. Turbolinks has been rewritten from the ground-up
If you’re using Turbolinks, as we are, there are a number of things that will break. I highly recommend you reading through README on GitHub. You would need to update to the new Turbolinks events and data attributes. Unfortunately, the page:update and page:change events that we’ve been relying on are no longer there, so for now, I just switched back to Turbolinks Classic.
Bringing in new features
Once we had our application upgraded and working, it was the time to think about new features. The great thing is that you can do it at your own pace, or even postpone as we did with Turbolinks.
Here are the things I’ve brought into our application right after the upgrade since our codebase is small and it wasn’t really time-consuming. The details of these features are outlined in Mario Alberto’s post I mentioned earlier.
- I’ve enabled back “belongs_to required by default” feature (see above) and cleaned up validations. You no longer need to explicitly validate “presence” of the associated object.
- I’ve disabled ActiveRecord’s “halting callback chains” feature (see above), revised that no callbacks relied on halting with a boolean flag and instead raised ActiveRecord::Rollback where it was appropriate.
- I’ve also brought in the has_secure_token and got rid of our own Concern that was responsible for generating application tokens.
5. Wrapping up
Ok, this brings us to the end of this post. I am sure there is probably a bunch of other issues that I’ve overlooked with the upgrade, which we’ll find as we go. And you know, it’s normal, because software has bugs. I just hope that this post has helped you squashing more of them 🐛, before they reached the other end.