No App Left Behind: Upgrade your Application to Ruby 3.0 and Stay on the Upgrade Path!

Glen Crawford
January 13, 2021

One of the best things about being a Ruby developer is that every year, without fail, we get a Christmas present in the form of a new release of the Ruby programming language on December the 25th. And this Christmas was no different, with Ruby 3.0.0 being released on Christmas Day 2020!

Ruby 3 is a big release and has been in the works for a long time, and there are by now already a million blog posts about its contents, so I don’t intend to spend time here analysing the new features and fixes. I want to relay our early experiences with the upgrade, and reiterate why you shouldn’t put it off.

Yesterday was the first day back for most of reinteractive after the Christmas and New Year holidays, and as per normal, we have multiple projects on the go. The problem is, with such a significant release, it’s probably best to start with something safe, and use it as a test case before upgrading the rest of our apps. Luckily, we have a project that is still in its initial development, and as such only has a staging environment, meaning no real users or data in production. It also has 100% test coverage. Couldn’t get more perfect! I took a few hours yesterday to perform the upgrade, and had it deployed by the end of the day, making for a good start to the new year!

Upgrading to Ruby 3.0.

This might be a little controversial, but in terms of what you will need to do in order to upgrade, the secret of Ruby 3 is that it really just finished the job of 2.7. Sure, you get some fancy new things like the performance boost, Ractor for concurrency, the (sort of) type checking provided by RBS, and (my favourite) single-line “endless” method definitions, and you’ll want to use all of those going forward. But the biggest thing you will have to deal with when upgrading is the separation of positional and keyword arguments. These changes aren’t unexpected; Ruby 2.7 started to print warnings when it detected behaviour that would break in 3.0, meaning that it was deprecated in the former and finally removed in the latter.

What this means is that the most important step of the Ruby 3.0 upgrade is not to simply upgrade to 3.0, but to upgrade to 2.7 first. Sometimes it can be tempting to do one giant leap (e.g. 2.6 –> 3.0) rather than a few small hops (e.g. 2.6 –> 2.7 –> 3.0), but do not do that in this case, as you will miss these deprecation warnings from 2.7. And these aren’t the kind of warnings that you can safely ignore; they are explicitly about behaviour that will fully break on 3.0, both in your application and that of its dependencies.

Thankfully, while Ruby 3.0 is new, 2.7 has been out for a year, so all the popular gems (libraries of Ruby code that provide pre-written functionality) that your app depends on for authentication, searching, and so on, have already been updated to resolve the deprecation warnings. This means that you can safely upgrade by following the below process:

  1. Upgrade your application to the latest patch release of Ruby 2.7, which is currently 2.7.2.

  2. Run your test suite and click around the app for a while. Look for deprecation warnings coming from your own code and fix them all up until there are no more.

  3. You will see more warnings coming from gems. If so, it’s likely that the gem has already released a new version that resolves the deprecation. Check what version of the gem you are using, and upgrade to the latest patch release. This is where the pessimistic version constraint operator comes in handy: if you specify a gem dependency with ~> 2.0.3 for example, then it’s trivial to run bundle update <gem name> to upgrade to the latest patch release of the gem (e.g. 2.0.5), without upgrading to a new major or minor release (e.g. 2.1.0), which may introduce incompatibilities. Keep doing this for each gem reporting these deprecation warnings until you don’t see any more.

  4. Now you’re good to upgrade to Ruby 3.0! If there are any more outstanding cases that you didn’t catch on 2.7, then at this point the app will fully error rather than simply warn you, likely preventing the app from even booting. Fix as necessary until the app boots, your tests pass, and you are able to click around the app without errors.

About Ruby and Rails Upgrades in General.

One of the points that we often make with clients is that it’s generally best to stay on top of Ruby and Rails upgrades, and try to do them fairly soon after release. Granted, that doesn’t mean it has to be on day one, and there is good sense to the common wisdom of waiting for the first “patch release” (e.g. 3.0.1 instead of 3.0.0) before upgrading. But that needs to be weighed against falling behind. If you leave upgrades too late they will accumulate, and before you know it, you need to do five years worth of both Ruby and Rails upgrades, which could take months, sometimes amounting to an almost total rewrite of your application. And as for deployment, you’ll have to deal with downtime, user disruption, data migration, and so on. It’ll be like trying to turn a battleship in a bathtub. It’s far easier to just stay on top of upgrades as they come out; you’ll likely find that it will take only a few hours a couple of times a year.

As an aside, the Ruby and Rails versions of your application are one of the first things we look at when performing our AppReview service. And if I’m the reviewer, then you can likely guess what my first two recommendations will be!

I know it can be hard to justify spending time and money on upgrades. If your app works now and you don’t need any new features, why bother? Think of it more as risk mitigation for your business. All software projects, including Ruby and Rails, have maintenance policies that phase out old versions after a couple of years. If you get too far behind, you’ll find yourself cut off from bug fixes, security patches, and performance improvements, and that’s a huge risk to your business. We can help. Contact us at reinteractive and we’ll help you get you back on the upgrade path. We’ve done it all before, and now we’ve done it for Ruby 3!