blueprint

BiggerPockets product & engineering blog

How we upgraded the BiggerPockets platform to Ruby 3.0

Ruby is the birthstone of July, so it’s only fitting that we took our time last month to focus on getting up to date on the latest version of our favorite programming language: Ruby. This article only covers our Ruby 2.7 to 3.0 upgrade experience, but that was immediately followed with a completely painless upgrade to Ruby 3.1 less than 24 hours later.

Research

Ever since I started at BiggerPockets in January 2022, getting on the latest Ruby version was high on my to-do list. 2022 is the last year for support of Ruby 2.7, and my experience tells me that it’s really important to keep up with this fast-paced community.

Karl Entwistle, another BP engineer, also did research and shared this 2021 GitLab article that ended up having critical advice for performing this upgrade.

Planning

To kick off planning, I created a branch that switched us over to Ruby 3.0. As soon as I pushed it, our CI server (CircleCI) went to work building and executing our automated RSpec suite. Any failures here gave me a good idea of the scope of work and a rough estimate of how long it would take to upgrade. Based on the number of errors, I guesstimated that we were looking at a few days to do the upgrade, and the critical keyword arguments change would need to be carefully done by hand.

Execution

Due to the risks involved, I decided to make this a two-stage upgrade: one stage to run Ruby 2.7 in production but log all deprecation warnings, and a second stage to do the actual interpreter upgrade.

Stage One - Log keyword warnings as Sentry exceptions

Based on Karl’s reseach, I created a file in lib/warning.rb with this content:

# frozen_string_literal: true

module Warning
  raise "Remove this file" if RUBY_VERSION.split(".").first.to_i > 2

  alias_method :original_warn, :warn

  # From https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18
  KWARG_WARNINGS = /: warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z/.freeze # rubocop:disable Layout/LineLength

  class KeywordArgumentDeprecation < StandardError; end

  def warn(message)
    if KWARG_WARNINGS.match?(message)
      Sentry.capture_exception(KeywordArgumentDeprecation.new(message))
    else
      original_warn(message)
    end
  end
end

By adding this file, I was able to leverage our production error warning system to alert us whenver we had incompatible Ruby 3.0 code.

Stage Two - Upgrade

With a few days of zero warnings in Sentry, I felt confident enough to actually do the 3.0 upgrade. This really just involved updating a few configuration declarations, which will vary from app to app. Heroku just makes this so easy.

Problems encountered

It took a few days for anyone to notice, but there was a problem! Normally our website sees tens of thousand organic visits every day to our homepage from Google. Us engineers don’t keep a close eye on this metric, but other folks here at BiggerPockets do. They followed the normal issue reporting process, but because this took a few days, I didn’t associate the drop in metrics with the upgrade to Ruby 3.0, especially because only some traffic was affected. Another engineer here (John Gallagher) actually did the diagnosis and tracked it back to an ill placed bit of legacy code that slipped through because it only fired during exceptions and silently dropped invalid JavaScript onto the page.

Lessons learned

This is a major upgrade. Work with product ownership to get the upgrade on the roadmap early. Product ownership may not understand why the upgrade is necessary, as an engineer it’s your job to make sure they know how important it is, what the risks are, and how long you expect it to take.

High test suite coverage is essential to moving quickly with confidence.

Untested error handling is often a source of bugs.

If possible, don’t rely on one engineer to spearhead this effort. This upgrade will affect all Ruby engineers on a project, so it’s great to get multiple engineers and teams involved.

Conclusion

With it’s deprecated keyword argument casting way back in December 2019, the Ruby 3.0 upgrade was the riskiest upgrade in recent memory.

We took a methodical approach to executing this upgrade in an almost bug-free way. One of the great things about working at BiggerPockets is a long-term commitment to automated tests. Over the years, this engineering commitment has granted us the ability to iterate rapidly with confidence.

Before working at BiggerPockets, I shied away from browser-based test suites due to their slow performance, but now I am convinced that the costs are worth it. It’s very easy for a CI service to spin up dozens of VMs and quickly churn through a suite.

Finally, this wouldn’t have gone nearly as quickly if we didn’t have a simple cloud native architecture (we use CircleCI and Heroku) that handled all the difficult operational things for us.

Further reading

Banner image source