RailsConf 2018

Ten years of Rails upgrades

Ten years of Rails upgrades

by Jordan Raine

The video titled "Ten years of Rails upgrades" is presented by Jordan Raine at RailsConf 2018, focusing on the evolution and challenges of upgrading Ruby on Rails over a decade. Raine, a Ruby developer at Clio, shares insights from personal experience of upgrading various versions of Rails, highlighting both improvements and ongoing difficulties in the upgrade process.

Key points discussed throughout the video include:
- The Evolution of Rails Upgrades: Raine notes that with each version, Rails has improved its upgrade path through better deprecation warnings and extended beta testing periods, making it theoretically easier to upgrade.
- Real-World Experiences: He contrasts the experiences of a small startup and a larger company during their Rails upgrades, emphasizing that while the steps taken (upgrading gems, changing code, testing, deploying) are similar, the scale and resulting challenges can differ greatly based on the company’s size and code complexity.
- Technical Debt and Maintenance: One critical issue Raine emphasizes is the accumulation of technical debt during the long gaps between upgrades, which complicates future upgrades. He illustrates this with anecdotes about outdated gems and deprecated methods proliferating across an aging codebase, raising concerns about dependency management.
- Strategies for Overcoming Challenges: Raine suggests practical strategies to facilitate smoother upgrades, including:
- Keeping the gem file healthy by avoiding version constraints and eliminating forks.
- Actively addressing deprecations rather than deferring them until future upgrades are required.
- Implementing a systematic approach to "dual boot" the application in the next Rails version to catch issues early.
- Cultural Shift in Upgrades: He emphasizes fostering a culture where regular gem updates and gradual upgrades become part of the development process, translating the upgrade effort from isolated events to continuous work.

Raine concludes with optimistic reflections on the agility gained from applying these principles. He reinforces that by continually addressing dependencies, promoting teamwork in upgrades, and engaging with the Rails community, future upgrades can be less daunting and more integrated into the development workflow.

The main takeaways from Raine's talk focus on the need to recognize the importance of managing dependencies, embracing a culture of regular updates, and realizing that upgrades can be a collaborative effort across the entire development team rather than the responsibility of a few individuals. By shifting mindset and practices, companies can better prepare for Rails upgrades and improve codebase quality over time.

00:00:11 Hello, my name is Jordan Raine. I work at a company called Clio, and I'm a developer there.
00:00:18 I've been writing Ruby code and been a Rails developer for about nine years. Our company is based in Vancouver, Canada.
00:00:25 Over those nine years, I've had the chance to upgrade apps from Rails 2 to 3, from 3 to 4, and from 4 to 5.
00:00:34 I can still remember the Rails 2 to 3 upgrades and how many breaking changes there were. It was painful, but thankfully, things have gotten better.
00:00:50 Now, there are fewer breaking changes for each version as we receive updates. We also have deprecation warnings that are becoming clearer.
00:01:02 In many cases, these warnings will tell you exactly what you need to do to ensure that your app will work with the next version of Rails.
00:01:14 In addition, we now have beta periods that can last for months, allowing us to test the newest version of Rails.
00:01:20 Yesterday, when I spoke, the idea was mentioned that Rails isn't dying; it's maturing. Looking at the upgrade path, this certainly seems to be the case on paper.
00:01:33 In practice, however, upgrading can still be really hard. It's hard for us at Clio as well.
00:01:40 After discussing this with other companies, I learned that it has been hard for them too.
00:01:47 I was chatting with someone this week and found out that there are a lot of consultants who step in to perform upgrades for companies because some simply can’t afford to do it or don’t know how to navigate their own codebase.
00:02:05 This led me to question: with all the improvements made by the Rails core team, why is it still tough to upgrade?
00:02:12 I want to explore this issue in four parts, starting with a story of two Rails upgrades.
00:02:23 The first story involves a small startup.
00:02:28 One day, a developer is scrolling through Twitter and sees a tweet from DHH: Rails 5 is out.
00:02:37 She has been meaning to try out Active Storage and, not having much on her plate, decides to upgrade.
00:02:44 She spends the rest of the day upgrading gems, running the tests, making them pass, and sending it off for code review.
00:03:00 The next day, she and another developer work through all the major flows in the app and get it running.
00:03:07 By the end of the day, they ship it. Just 24 hours after seeing that tweet, they're running the latest version of Rails.
00:03:20 The next story features a different company.
00:03:27 This company is larger and has been around for many years, with a big team of developers and thousands of customers who rely on their services.
00:03:44 When they see the tweet from DHH in their Slack channel, they don’t feel excited; instead, they feel dread.
00:03:52 They realize they are now two minor versions behind and one major version behind, running out of maintenance time.
00:04:00 They can’t defer the upgrade any longer and gather a team to tackle the project bit by bit.
00:04:07 As they upgrade their codebase, they try to ship any changes that they can, but the process takes three months.
00:04:20 Once they finally finish, they haven’t deployed a new Rails version in many years, and so they’re uncomfortable.
00:04:30 They decide to schedule downtime just to ensure the deployment goes smoothly.
00:04:39 When they finally deploy, they are now running the latest version of Rails.
00:04:50 It may not seem fair to compare these two upgrades, but the startup only had a couple of gems and a relatively simple codebase.
00:05:05 Their tests were few and the stakes were lower, allowing them to take larger risks.
00:05:17 In contrast, the larger company faced numerous gem upgrades, including some that were no longer maintained.
00:05:30 They had thousands of lines of code to review and many tests, some of which did not even start to work.
00:05:44 When they were ready to deploy, due to the reliance on countless customers, the anxiety was palpable.
00:05:56 Yet, both companies fundamentally went through the same steps: they upgraded their gems, modified their code, tested it, and deployed.
00:06:05 One company had more to do at each step, but the end result was still the same. They were both now running a new version of Rails.
00:06:17 This led me to wonder whether this experience is an inevitable part of growing a company and aging codebase.
00:06:35 That brings us to Clio. Our upgrade paths look a lot like that of the larger company.
00:06:47 We’ve been around since 2008, building a product for lawyers.
00:06:54 When I began to look at our next Rails upgrade earlier this year, I started to ask why this process seemed so difficult.
00:07:05 What have we done in the past, and how can we improve it? I went back to explore our upgrade history.
00:07:19 To bring you up to speed, I’ll give you a quick look at our upgrade path and timeline.
00:07:26 This timeline starts with Rails 2.0 in 2007, which is the version we shipped with when the product first launched.
00:07:34 On the very same day that 2.0 was released, our founder upgraded our app to 2.0.
00:07:43 Halfway through 2008, when 2.1 came out, we again upgraded on the same day.
00:07:51 2.2 came out a couple of months later, and we lagged a little bit, about three months behind.
00:07:58 But we soon caught up. Then 2.3 came out, and we lagged a bit more, nine months this time.
00:08:06 Next came 3.0, followed by 3.1 and 3.2, and in 2012, we felt some pressure.
00:08:15 We upgraded to 3.0, 3.1, and 3.2 over a five-month period.
00:08:25 Then 4.0 was released, followed by 4.1, and then 4.2.
00:08:31 In 2015, we upgraded to 4.0, 4.1, and 4.2 over a six-month period.
00:08:42 This was too fast.
00:08:49 Looking at this timeline, an obvious pattern emerges.
00:08:55 What surprised me, however, was not the times we were actually upgrading in 2012 and 2015.
00:09:05 Instead, it was the gaps in between the years.
00:09:16 I found myself asking what we were doing during those gaps and why we didn't upgrade the app.
00:09:25 When I thought about the business at that time, it turns out that we were actually quite active.
00:09:30 We were growing as a company, hiring developers, building features, and adding customers.
00:09:40 We had started to generate revenue, and our business choices seemed solid.
00:09:51 However, we also made some questionable technical decisions, and I believe the key to easily upgrading Rails lies in these gaps.
00:10:01 For those who haven’t experienced a Rails upgrade project before, let me illustrate what it feels like to be on a Rails upgrade team.
00:10:17 Upgrading your framework impacts you significantly, as you need to pay off technical debt accrued over the years.
00:10:24 When you open your Gemfile, you often find outdated gems that have been left untouched for too long.
00:10:36 It's tempting to add gems when you need specific functionality and then ignore them as time passes.
00:10:44 But problems arise when your codebase ages and those gems start causing issues.
00:10:52 This highlights the principle that dependencies are not free. It's not a new idea; we know this.
00:11:03 There is a fascinating blog post from 2001 by Joel on Software discussing the Excel development team's approach to dependencies.
00:11:14 They advocated eliminating dependencies and took it to the extreme of building their own C compiler.
00:11:23 This seems absurd for those of us working with web apps today, but it was quite effective for them.
00:11:31 The Excel team's mentality of independence meant they shipped on time and maintained uniformly high quality.
00:11:40 They recognized the associated costs of their dependencies and made a conscious decision to manage them better.
00:11:56 While we shouldn't aim to empty our Gemfile of all dependencies, we do need to understand these costs.
00:12:08 It's not practical to build our own web framework; I'd prefer using Rails. But commitment to regular updates is essential.
00:12:20 Failing to keep up with these updates leads to compounded problems. For instance, we integrate with QuickBooks Online using a gem.
00:12:36 QuickBooks Ruby relies on another gem called OAuth, which we had pinned to an older version.
00:12:45 While QuickBooks Ruby worked fine for a while, when we tried to upgrade it to add a bug fix, we ran into trouble.
00:13:00 The new version of QuickBooks Ruby upgraded the OAuth version, which caused it to fail.
00:13:12 This forced us into a dilemma: either we could not upgrade any gem, leave the bug unfixed, or upgrade both gems, blowing up our timeline.
00:13:24 Ultimately, we chose to fork the gem and downgrade the version of OAuth, which seemed like a sensible solution at the time.
00:13:38 While this worked temporarily, it deferred the costs, ultimately coming back to bite us during future upgrades.
00:13:49 Each future upgrade would require going through the fork, merging codes, and updating versions in a cumbersome way.
00:14:02 This made developers less likely to upgrade because it was such a painstaking process.
00:14:09 The more rigid your dependencies are, the harder they are to maintain, which fosters resistance among developers to make necessary changes.
00:14:25 Another thing we've done in those gaps was continuously merge more and more code.
00:14:35 When you ask a developer to implement a feature, they'll often copy code patterns they find within the existing codebase.
00:14:45 If that code includes calls to deprecated methods or undocumented Ruby calls, those bad patterns will spread throughout the code.
00:14:55 Every time this pattern propagates through the code, it becomes harder to clean up.
00:15:02 Some changes can be resolved with simple find-and-replace methods, while others, like layers of protected attributes, are difficult to remove.
00:15:13 When we upgraded from Rails 3 to 4, we added protected attributes to give us temporary relief, but that only compounded the problem.
00:15:27 Even when we removed it from our models, it had affected the entire application.
00:15:39 Moreover, I know I'm not alone in this; many developers have had the same experience as that gem has been downloaded millions of times.
00:15:51 This disconnect from the Rails community often leads us to solve problems that Rails has already tackled in better ways.
00:16:07 This happened during our upgrade to Rails 4.1 when we missed the introduction of Active Record enums.
00:16:19 Instead of utilizing the built-in functionality, we built our own version, leading to multiple similar implementations.
00:16:31 Even when Active Jobs were introduced in 4.2, we skipped it because we had already invested in our own solutions.
00:16:43 Creating your own solutions is fine if it's core to your mission, but doing so just to catch up translates to wasted effort.
00:16:57 Additionally, this complicates onboarding for new developers who might find certain solutions within the codebase unrecognizable.
00:17:10 Having looked into this, I found that these gaps discourage us from performing the upgrades we know we ought to.
00:17:18 But the good news is that we can manage most things needed for Rails upgrades at any time.
00:17:26 You can undertake them in small steps; they don't have to be massive projects.
00:17:38 At Clio, we approached this in three specific ways.
00:17:46 First, we focused on maintaining a healthy Gemfile.
00:17:54 Dropping unnecessary version constraints is a good start, ensuring that your Gemfile accommodates upgrades.
00:18:01 Whenever feasible, consider if a version constraint is truly necessary and remove it when possible.
00:18:10 Second, we eliminated forks which often complicated things even more.
00:18:24 Forks create obstacles for upgrades, increasing the burden on developers, who may not remember the need for them.
00:18:33 Finally, we fostered a culture of eagerly updating gems.
00:18:39 Instead of waiting for a feature or bug fix, we made it a priority to update gems regularly.
00:18:49 Awareness of outdated gems is crucial, and we utilize tools to help with this process.
00:19:02 One such tool is 'bundle outdated,' which provides a line indicating each outdated gem.
00:19:21 However, running this can be overwhelming, especially when numerous gems are outdated.
00:19:32 Inspired by this, we developed our own script called 'bundle report' to better manage our Gemfile.
00:19:48 This reports gems from oldest to newest, giving clearer priorities on which gems need updates.
00:20:01 Next, we focused specifically on Rails upgrades.
00:20:14 By identifying gems that won't work with new versions of Rails and what their acceptable version is, we could manage changes better.
00:20:28 This systematic approach enables us to act proactively to upgrade.
00:20:35 We also focused on deprecations, adhering to the notion that they have been improving over time.
00:20:44 Most major changes can be caught if you're running the latest patch version of the previous Rails version.
00:20:53 In our case, we developed a deprecation tracker that integrates with our tests.
00:21:00 This tracker monitors deprecated methods across our codebase and alerts us during Continuous Integration.
00:21:11 By having control over the deprecated methods, we can keep our coding practices in check.
00:21:27 This provides us substantial insight into our code quality and encourages proactive maintenance.
00:21:35 The last approach we adopted was the concept of dual booting Rails.
00:21:48 First introduced by Shopify, this allows developers to easily switch between versions of Rails.
00:22:00 It's beneficial for testing various configurations with minimal friction.
00:22:15 By making these adjustments, it now feels less daunting to upgrade Rails.
00:22:24 These methods enable us to continuously learn from previous upgrades and lessen the burdens faced with each version transition.
00:22:36 Moving forward, our Rails upgrade process did not have to seem like an overwhelming endeavor.
00:22:44 These strategies, while simple, foster a supportive culture of constant improvement.
00:22:57 In relation to that other company, by adopting these strategies, they'll have significantly less to handle during their next upgrade.
00:23:10 Thanks to the recently applied strategies, they’ll approach upgrades with confidence.
00:23:19 They’ll experience lower resistance during the upgrade process thanks to the tried and tested methods.
00:23:30 Ultimately, these changes lead to not only a smoother upgrade process but also a more maintainable codebase.
00:23:39 The landscape of ongoing Rails upgrades will begin to feel more manageable, even when large updates roll out.
00:23:50 Instilling this mindset throughout the organization benefits both the collective growth of the code and the morale of the team.
00:24:03 As we evolve our strategies around Rails upgrades, the act of upgrading becomes less of a crisis and more of a continuous process.
00:24:10 I believe solidifying these principles will make our codebases cleaner and our development experience more enjoyable.
00:24:20 I encourage everyone to keep the conversation going around Rails upgrades—thank you!