Rails Upgrades

Summarized using AI

Upgrading GitHub to Ruby 2.7

Eileen M. Uchitelle • December 18, 2020 • Online

In this video titled 'Upgrading GitHub to Ruby 2.7', Eileen M. Uchitelle, a Staff Software Engineer at GitHub, discusses the complexities and strategies associated with upgrading from Ruby 2.6 to Ruby 2.7. The session highlights several key points:

  • The Challenge of Upgrading: The transition to Ruby 2.7 involved addressing over 11,000 warnings in GitHub's codebase, emphasizing that the upgrade process can be overwhelming but is not inherently high risk.

  • Importance of Upgrading: Eileen explains that keeping applications updated prevents technical debt, enhances performance, and contributes to a more stable and scalable system.

  • Major Deprecations: Key deprecations in Ruby 2.7 include changes to positional and keyword arguments and URI methods. Eileen elaborates on how to resolve these warnings, offering practical coding examples.

  • Custom Tools and Processes: To streamline the upgrade, GitHub developed a custom monkey patch for the warning module, which allowed for better tracking and management of warnings. She also discusses a dual boot process that enables testing of different Ruby versions concurrently, reducing merge conflicts and risk during the upgrade.

  • Collaboration and Delegation: The process involved collaboration across teams, allowing them to fix the numerous warnings efficiently. GitHub also sent upstream patches for external gems encountered during the upgrade instead of forking them.

  • Deployment Strategy: Eileen outlines careful deployment strategies, including a phased rollout to monitor for issues in production while emphasizing the importance of testing in a staging environment before the final upgrade.

  • Performance Improvements: Post-upgrade, significant performance enhancements were reported, including a reduction in application boot time and decreased object allocation, which can contribute to overall application efficiency.

  • Future-Proofing Ruby: Eileen concludes by encouraging open-source contributions to improve Ruby for everyone, emphasizing the need for continuous upgrading and the collaborative nature of the Ruby community.

The session underscores the notion that while upgrades may pose challenges, they are essential for the long-term viability and performance of applications, especially at scale like GitHub.

Upgrading GitHub to Ruby 2.7
Eileen M. Uchitelle • December 18, 2020 • Online

It's no secret that the upgrade to Ruby 2.7 is difficult — fixing the keyword argument, URI, and other deprecation warnings can feel overwhelming, tedious, and never ending. We experienced this first-hand at GitHub; we fixed over 11k+ warnings, sent patches to 15+ gems, upgraded 30+ gems, and replaced abandoned gems. In this talk we’ll look at our custom monkey patch for capturing warnings, how we divided work among teams, and the keys to a successful Ruby 2.7 upgrade. We’ll explore why upgrading is important and take a dive into Ruby 2.7’s notable performance improvements.

Eileen M. Uchitelle
Eileen Uchitelle is a Staff Software Engineer on the Ruby Architecture Team at GitHub and a member of the Rails Core team. She's an avid contributor to open source focusing on the Ruby on Rails framework and its dependencies. Eileen is passionate about scalability, performance, and making open source communities more sustainable and welcoming.

RubyConf 2020

00:00:09.440 Okay, here we are! Oh my God, can you believe how fast this day is already moving? I don't even look at the clock because I think it's lying to me at this point! Anyway, a lot of things are continuing to happen. The first thing I want to mention is that I put a challenge out there for the RubyConf virtual 5K: if 20 people or more finished a 5K run/walk or did 30 minutes of exercise, I would reenact the entirety of 'Maniac' from Flashdance. So, we've hit 35 participants, which means I now have to do that! I've got some preparation to do.
00:00:58.480 Here we are! I want to introduce Eileen Uchitelle. Eileen is one of the big staples in the Rails and Ruby community. If you haven't been familiar with Eileen before, a lot of her talks dive deep into the inner workings of the frameworks that we've come to know, love, and appreciate. So, I want to welcome Eileen to the stage.
00:01:10.720 Hi everyone! As Adam said, I'm Eileen Uchitelle. You can find me online at the handle ‘Eileen Codes’. I'm a Principal Engineer at GitHub where I work on making improvements to the Rails framework and Ruby language to ensure GitHub is stable, resilient, and scalable. In addition to my work at GitHub, I'm also a member of the Rails Core Team. For those of you who are unfamiliar, this group decides the future of the Rails framework, figuring out what features we want in the next release and making sure that they happen.
00:01:35.520 Today, I'll be talking about the recent Ruby 2.6 to 2.7 upgrade that we did at GitHub. We'll dive into why this upgrade was particularly difficult, the processes we used to ensure the upgrade happened as efficiently as possible, how we deployed our upgrade, and lastly, what we're doing to make sure the next Ruby upgrade is better for everyone. Most importantly, I hope to dispel the myth that upgrading your application is inherently high risk.
00:02:05.360 The GitHub application was born in 2007 using Rails 1.2 and Ruby 1.8. Sometime in 2009, GitHub decided to fork both Rails and Ruby. This seemed like a simple decision at the time because the team didn’t need to alter the existing code to work with the framework and language changes.
00:02:29.760 However, as GitHub's forks deviated more and more from upstream, making changes in the application became increasingly difficult. If you're interested in the challenges we faced running on a custom fork, you can watch my 'Past, Present, and Future of Rails' talk from RailsConf 2019. For this talk, however, I'll be focusing on how we approached a Ruby upgrade at GitHub.
00:03:05.840 I hope this talk will show you that while upgrades can be really difficult, they are not inherently high risk. The payoff for staying upgraded is always better than forking or falling behind. Because we did the hard work of upgrading and getting off a fork, we are committed to staying up to date for the future of our company, our engineers, and our product.
00:03:40.560 Falling behind the most recent release is no longer technical debt that we're willing to accept as an engineering organization, even when we know the upgrade is going to be difficult. The challenges we faced in upgrading from Ruby 2.6 to 2.7 were not unique to GitHub; many gems, libraries, and companies had or will have the same problems that we did. Ruby 2.7 doesn't have a ton of breaking changes, but the biggest challenge was that core methods now return frozen strings, so we had to update a lot of code and gems.
00:04:14.640 However, getting a green build wasn’t what made our upgrade difficult. At GitHub, we're committed to running deprecation-free, which means that in addition to a green CI build, we want to eliminate all future breaking code as well. We take this approach for a couple of reasons: it makes upgrading easier for the next version by preparing our application for the future.
00:04:56.079 If you wait to fix deprecations, they might end up being a blocker in the next version. Deprecations are also incredibly expensive. Recently, I made a change to Rails, and when I benchmarked the deprecated method versus the new method, the deprecation warning was responsible for making the legacy method two times slower than the new method, even though without the deprecation they were basically the same speed. Running your application with deprecations can have serious consequences on your performance, depending on how often the deprecated code is called.
00:06:30.720 Fixing the deprecations was a huge task due to the size of our application. When we started counting the number of warnings in our application, we found that GitHub had over 11,000 warnings that we needed to fix. Yes, you heard that right—over 11,000 warnings! That's quite expensive. Ruby is smart and only throws a warning once for a particular call site, so if you have a test that calls a deprecated method multiple times, it will only show you the warning once.
00:07:02.960 We also didn’t start counting until we had fixed a good chunk of warnings, so in reality, we probably had closer to 20,000 warnings. Before I get into how we approached fixing these, I'd like to go over some of the major deprecations in Ruby 2.7 that you'll need to address. The first major deprecation is the separation of positional and keyword arguments. At first, this might not sound like a big deal, but it means that option-style hash arguments and keyword arguments are no longer interchangeable.
00:07:41.760 Let's dive into what this deprecation looks like, what it means, and how to fix it. Here we have a class called ‘MyClass’, which has an initializer that takes two keyword arguments, part_one and part_two. In Ruby 2.6, if you were to initialize MyClass with a hash, we would see the string output with no warnings. However, if we call the same method with hash arguments in Ruby 2.7, we'll see the following deprecation warning: "Warning: the last argument as keyword parameters is deprecated; maybe ** should be added to the call." The called method ‘initialize’ is defined here.
00:08:25.440 The first time you see this warning, it might be kind of confusing because keyword argument warnings come in two parts. The first part identifies the caller that caused the warning, in this case, it's our call to ‘new’ on MyClass. The deprecation warning includes the file name and line number where the method was called, indicating that this method was called from ‘example.rb’ on line 13. The second part of the warning shows where the method being called is defined, telling us that the ‘initialize’ method defined on line two of ‘example.rb’ is expecting keyword arguments but instead received a hash.
00:09:03.360 Fixing the caller is simple once you learn the pattern for identifying the correct fix. The first way to fix the caller is to remove the curly braces, as shown here. We know the method accepts keyword arguments, so we can send keyword arguments directly. Sometimes, the arguments you have are hashes because they're passed from another method, and you actually want to use a hash. In this case, deleting the curly braces isn't going to work and might make your code unnecessarily complex.
00:09:57.760 If you have to send a hash, you can indicate to Ruby that the hash argument should be converted into keyword arguments by adding a double splat like this. This tells Ruby to convert the hash into keyword arguments before sending them to the defined method. The warning output can be confusing because it suggests that maybe you always want to add a double splat. Both fixes are interchangeable, but I prefer to delete curly braces whenever possible because it makes my code cleaner.
00:10:49.760 However, there are cases where updating the caller won’t fix the warnings and you'll need to update the method definitions instead. Here’s an example: we have a class called ‘AbstractClass’ that initializes a MyClass object. The initializer for MyClass takes keyword arguments, part_one and part_two. If we were to initialize an AbstractClass and pass keyword arguments to the initializer, we’d see the same warning we saw earlier. The warning might be a little confusing this time because we know that our caller is doing the correct thing by passing arguments that MyClass expects.
00:12:04.000 What’s wrong here? In this case, it’s the method definition for AbstractClass that needs to change, rather than the AbstractClass caller. To fix this warning, we need to update the AbstractClass to take a double splat instead of a single splat. In this example, I changed that argument name to be ‘quarks’ instead of ‘args’. This isn’t strictly necessary for Ruby to work, but it’s a good pattern to signal to future engineers looking at this code what the AbstractClass initializer actually expects.
00:12:44.480 The three cases we've looked at so far are relatively simple to fix, but there are some cases using delegation that complicate figuring out the source and fix for a warning. Let's look at Active Job as an example. Let's say you have a job class in your application called ‘MyClassJob’. The ‘perform_later’ method in MyClassJob takes keyword arguments. In Ruby 2.6 and below, this worked fine, even though the ‘perform_later’ definition takes a splat, but in Ruby 2.7, this will now produce a warning in Active Job.
00:13:30.320 We can’t just fix this by changing the arguments accepted by `perform_now` because that could break existing jobs if jobs are queued on Ruby 2.6 and then run on Ruby 2.7, which is often the case. To avoid the warning, Rails implemented keyword arguments on the perform later method. Active Job is fixed in Rails 6.0 and 6.1, so you shouldn't have to change any of your job code if your app is using keyword arguments. However, there's one tricky situation left with delegation: if you have a job that uses a single hash when it expects keyword arguments, the warning will incorrectly indicate that the caller is in Rails.
00:14:41.440 We encountered quite a few of these cases at GitHub, so we created a monkey patch that would allow us to see the entire stack trace for tricky cases like this. We'll take a look at that monkey patch later in this talk. Another deprecation we encountered fairly often in our application was the URI method deprecations in Ruby 2.7. Methods on URI, like 'encode', 'decode', 'escape', and 'unescape' are deprecated.
00:15:23.440 Interestingly, these deprecations have actually been in place for about ten years, but they remained silent until now, so many people continued using them. The warning for these is clear: these methods are obsolete. URIs are composed of multiple parts, and you might not want to escape or encode them all in the same way. For this reason, there's not a direct replacement for URI in Ruby.
00:16:12.000 Fixing this warning will require you to investigate your use case and find a replacement that meets your requirements. One potential fix is to replace your URI with ‘Addressable::URI’. In this case, Addressable URI and URI are interchangeable, but there are significant differences. One is that an Addressable URI will raise an invalid URI error if the URI includes invalid characters. This may not be problematic if you have been validating URI parts from your users, but if you haven’t, and a disallowed character sneaks in, you might start seeing errors in your application.
00:17:14.560 If this doesn't work for you, you might consider using something like CGI.escape, but be aware that this method will encode all characters, making its behavior quite different from URI's behavior. There is no 100% equivalent drop-in replacement for the URI method, so it's important to have good tests around your callers in your application to ensure you’re implementing the right solution.
00:17:54.480 Now that we've dove into some of the warnings you'll need to fix, you're probably wondering how GitHub approached fixing 11,000 warnings. That’s quite a lot of work! In the next section, we’ll dive into the processes and tools we used at GitHub to make our upgrade as smooth as possible. For a large application like GitHub, it doesn’t make sense to use a long-running upgrade branch due to the number of merge conflicts that would arise.
00:18:49.680 To solve that problem, we use a dual booting approach that allows us to switch which version of Ruby our application is using. We also apply this process for Rails upgrades. We set our Ruby version in a config file, specifying the SHA of the Ruby version we want to build from. We then use 'ruby-next' with an environment variable to switch Ruby versions.
00:19:29.280 With this setup, we can run any command with the new Ruby version, like rails server, rails console, or rails next. The only catch is that the first time you run it, you have to wait for Ruby to compile. This approach makes it easy to switch back and forth between versions to verify changes, ensuring that the changes work in both versions and simplifies debugging and fixing warnings.
00:20:06.960 In addition to running two versions of Ruby for local development and testing, this also allows us to add CI to test multiple Ruby versions. This CI build can be run manually with a Slack command, as shown here, or can run automatically upon push to the codebase. Using a build like this helps to ensure that regressions aren’t introduced by other teams writing new code, reducing friction and allowing engineers involved in the upgrade to focus on existing problems instead of feeling like they’re playing whack-a-mole.
00:20:58.080 The single most important tool we used to upgrade Ruby 2.7, however, was a monkey patch on the warning module. This allowed us to capture, investigate, and change the behavior of warnings in our application. As I mentioned earlier, we had over 11,000 warnings to fix. Our monkey patch provided the ability to catalog, categorize, and find owners for each of the warnings in order to get them fixed in a timely manner.
00:21:55.840 Deprecation warnings in Ruby are just standard output in your logs, and there's no way to distinguish between a deprecation warning and a language-level warning, like an uninitialized instance variable. The only way to determine the kind of warning you’re dealing with is to parse or regex it. The monkey patch for warnings looked like this.
00:23:02.080 Now that I’ve simplified the patch to avoid including GitHub-specific code, I can say that we always output the warning to standard error to maintain the existing behavior of the warning module. Next, we implemented the ability to raise on warnings instead of just printing the warning, using the RAILS_WARNINGS environment variable. We used this to prevent regressions in CI for any newly introduced warnings.
00:23:47.840 Lastly, we implemented the ability to output the entire stack trace if DEBUG_WARNINGS was set to true. This was helpful to troubleshoot issues involving keyword arguments related to delegations. As I previously mentioned with Active Job, many of the warnings seemed to indicate that Active Job couldn’t accept keyword arguments. By providing the stack trace, we could discover that the true source of the warnings was actually coming from our tests.
00:24:39.360 If we look at the test file, we can see that we simply needed to delete the curly braces to resolve the warning. Without this patch, it would have been much more difficult to locate warnings in delegated methods like this. Once we had our patch working to optionally raise warnings and output stack traces, we added more features to streamline our upgrade process.
00:25:30.720 We needed a way to collect and identify the tests that caused a specific warning in our CI builds so we could assign work to the rest of the engineering team. The warning structure only identifies the caller and the method definition, but we needed to know exactly which tests reproduced which warnings. We accomplished this by extending our monkey patch to search through the stack trace of the warning, finding the file names that ended with '_test.rb'.
00:26:32.960 We then pass that test file and the warning message to a module called 'Warnings Collector'. In the Warnings Collector, we open the warnings text file from the temp directory, loop through each line of data, and join the message and test file path with ASCII art for easier parsing later on. Lastly, the Warnings Collector calls the ‘process_warning’ script directly on the system. This script was pretty complex and GitHub-specific, so I've stripped everything out and presented just the essentials.
00:27:25.280 Essentially, this script loops through all the warning file paths, looking up the code owner from our CODEOWNERS file. Once we gather that information, the script generates markdown files for each team and code owner that have warnings to fix. The markdown files include a checklist of each test file that emitted warnings, the line the warning came from, and the warning output. The files are downloadable from our CI builds as artifacts.
00:28:19.200 We pasted the generated markdown into GitHub issues that we opened for the teams responsible for fixing them, detailing how to run the Ruby version and how to resolve their warnings in their respective files. This process enabled us to quickly and efficiently delegate work to the owning teams, track their progress in fixing the warnings, and ensure that we had a clear line of communication with them.
00:29:14.480 The majority of issues took under an hour to fix since 99% of the warnings could be resolved by simply adding a double splat or removing curly braces. In addition to resolving warnings from GitHub’s internal code, we found many warnings were coming from external gems. Warnings from gems fell into a few categories: some gems had been fixed, and we just needed to perform a simple upgrade; other maintained gems hadn’t addressed their warnings yet; and there were unmaintained gems that were abandoned and would never receive fixes.
00:30:17.760 If you’re doing a Ruby 2.7 upgrade—or really any upgrade—I recommend auditing your Gemfile before beginning. Address as many outdated gems as possible. Even without an upgrade, it's essential to ensure you're not dependent on unmaintained or abandoned gems that could become blockers later or present security issues.
00:31:12.640 Our script that collected and categorized the warnings also compiled warnings for gems, so we could easily identify which had to be fixed. For gems that had warnings and hadn’t been addressed yet, we sent upstream patches, most of which were accepted. When upgrading, it’s critical to fix gems properly by sending upstream patches, rather than forking and fixing on your own. Forking means that you could end up supporting that code base for a long time, making your next upgrade more painful.
00:32:05.840 We faced challenges using some unmaintained gems, so because we have a policy of no longer forking gems at GitHub, we decided to replace any abandoned gems. Depending on the gem, determining how it's used in your application can be complicated. It’s important to understand how a gem is utilized before searching for a replacement. In some cases, we completely removed the need for certain gems, while in other instances, we sought replacements or rewrote our own smaller libraries to fulfill everything we needed.
00:33:04.480 We were able to accomplish this alongside the upgrade because we delegated known warnings to other teams. Those teams could focus on fixing warnings while the upgrade team concentrated on more challenging or time-consuming problems. Once we completed the upgrade and resolved all the warnings, we needed a way to prevent regressions. As I mentioned earlier, warnings are just output in the test environment, so we couldn’t rely on someone merely noticing if they introduced a new warning.
00:34:11.200 To address this, we used the RAILS_WARNINGS environment variable from our monkey patch to raise an error if any new warnings were introduced. This ensures that the upgrade team doesn’t need to keep fixing warnings until we’re running on Ruby 3.0.
00:34:28.240 After completing the upgrade, it was time to deploy. This can feel intimidating because an upgrade alters your underlying system. However, at GitHub, we've handled many Rails and Ruby upgrades, most of which involved no downtime or customer impact due to our exceptional deployment team and the processes we've developed with them over the years.
00:34:59.840 Deploying an upgrade is not inherently riskier than any other deployment. Being afraid of deploying an upgrade is not a valid reason to refrain from doing so. If your upgrades are risky, focus on improving your monitoring and building processes until it feels natural. The most critical factor that builds confidence in deploying an upgrade is the dual boot process, as it allows us to implement small, incremental changes to our code base.
00:36:31.360 We can merge changes into the main branch over time, meaning when we finally switch versions, we have a smaller diff for the final deployment. Because we have a dual booting setup in our CI, we also know that all changes going into GitHub daily are compatible with Ruby 2.6 and 2.7. This dual-boot strategy, compared to a long-running upgrade branch, drastically reduces risk by allowing you to implement smaller changes over time.
00:37:18.080 With this approach, we can push all changes to production before changing the version itself. Along with making smaller changes, we built confidence in our deployment through testing in a staging environment. We deployed the upgrade to a staging environment and asked the same teams that fixed warnings to use it for 20 minutes to an hour. While we are all GitHub power users, teams that know their area of the product best are excellent at ensuring everything works as expected.
00:38:04.960 During this process, we discovered a few new warnings that hadn’t been identified by our tests, although we didn’t find any regressions in the customer experience. While it might be tedious to spend time manually verifying during the upgrade in a production-like environment, this builds confidence and reduces risk for the final deployment.
00:38:49.760 Once we were done testing, it was finally time to deploy to production. For the upgrade, we used a slow, incremental, and manual rollout process. Our standard deployment process automatically rolls out to two percent, then twenty percent, and finally a hundred percent of traffic. However, for Ruby upgrades, we prefer a more manual approach.
00:39:43.680 We first deployed to two percent of traffic and planned to wait 15 minutes before increasing to a higher percentile. Almost immediately, however, we encountered a new exception—a frozen string error—stemming from a part of the code base that was missed due to a lack of tests around a specific edge case. Consequently, we rolled back the deployment to zero percent.
00:40:33.760 Because we deployed to such a small percentage, fewer than ten exceptions occurred, affecting only a handful of customers. Once we fixed the exception and deployed it to production, we resumed the incremental rollout process. As with our initial deployment, we started with just two percent of traffic and waited 15 minutes. After confirming no exceptions and getting no reports of issues, we rolled out to thirty percent of our Kubernetes deployment partitions.
00:41:34.720 This step was different from the previous two percent of traffic since some of our legacy partitions aren’t Kubernetes-based and require closer monitoring during deploys of Ruby upgrades, which is why we tested those separately. Upon confirming no regressions at thirty percent, we then deployed to another thirty percent, totaling about sixty percent of our Kubernetes partitions. Again, we experienced no regressions at sixty percent.
00:42:20.320 We then proceeded with our legacy hosts, deploying to about thirty percent of the non-Kubernetes partitions. Deployments for these may take up to 15 minutes. However, we initiated the deploy process on the quicker partitions, knowing we could roll back if any issues arose. Much like previous deployments, we experienced no errors or customer reports during this time.
00:43:20.480 Finally, after two hours of deployment, we upgraded to 100% of the partitions and merged the Ruby 2.7 upgrade. If this deployment sounds a bit boring, it is—because we built strong, consistent processes for deploying higher-risk changes, those high-risk changes become lower-risk.
00:44:24.960 We opted for a careful, boring deployment; aside from one initial frozen string error, we encountered no regressions in production. After all our hard work, the upgrade was a huge win! Along with achieving a shiny new Ruby version and a smooth deployment, we also received numerous benefits from upgrading.
00:45:33.120 The first notable change after deploying Ruby 2.7 to production was impressive performance improvements. We saw a decrease of 20 seconds in the application's boot time in production: before the upgrade, boot time was about 90 seconds, while after the upgrade, it dropped to 70 seconds. Note that this refers to production boot time, not development time.
00:46:25.920 While faster boot times don’t directly affect application performance, they do make deployments quicker, delivering changes to all our customers faster and contributing to a better deployment experience for engineers. This significant boot time reduction was partially due to John Hawthorne's optimizations to Ruby that avoid revisiting and clearing the method cache multiple times.
00:46:56.480 In addition to faster boot times, we also saw a decrease in objects allocated at boot time. Boot time object allocations went from about 780,000 to 668,000—112,000 fewer objects allocated at boot. Reducing object allocations is essential as they affect available memory, so it's important to keep these numbers as low as possible.
00:48:17.680 Ruby 2.7 also introduces various features beyond what Ruby 2.6 offers. One favorite, albeit small, yet impactful change is to the ‘method_inspect’ function, which allows you to view a method as an object and obtain information about the method. In Ruby 2.6, it provided the class and method definition, while in Ruby 2.7, it includes the arguments for where the method is defined.
00:49:13.120 I utilized this feature during the 2.7 upgrade to locate and understand a method signature in Capybara that was challenging to track down. This simple change makes the ‘method_inspect’ much more useful. Another great improvement in Ruby 2.7 is the new IRB interface. IRB now supports multi-line editing, for example, by allowing us to go up to our initializer and add an exclamation point.
00:50:10.240 Additionally, IRB features autocomplete, which displays the available options, and if you keep going, you can view the documentation for the chosen method within IRB. As you've likely noticed in the code examples, the syntax is also color-coded, making it easier to read and work with.
00:51:03.120 Ruby 2.7’s features wouldn’t be complete without mentioning manual GC compaction, which was developed by Aaron Patterson. Garbage collection compaction improves speed by defragmenting memory space. This means that a Ruby program doesn't need to search as far for objects in memory. If you're keen to try out manual GC compaction, simply call ‘GC.compact’. If you missed it, don’t forget to check out Aaron’s talk on the automatic GC compaction coming in Ruby 3.0, which is even better because you don't need to invoke this method manually; Ruby takes care of it automatically.
00:51:58.960 Keeping our application upgraded at GitHub doesn’t just improve performance and provide us with new features; one reason we upgrade is so we can contribute to making Ruby even better. We've been using Ruby since 2008, and we are committed to not only staying up to date but also fixing problems in the language that we experience firsthand. GitHub is one of the largest Ruby applications globally, but problems encountered at scale are not unique to GitHub.
00:52:53.920 I always remind engineers that Ruby is open-source; if you don't like how it works, change it! When we encounter issues in our language or framework, instead of working around them or expressing frustration, we should open an issue, engage in a dialogue, and submit a PR.
00:53:40.080 During our upgrade, I found working with deprecation warnings to be exceedingly tedious. There was no systematic way to ask a warning what type it was to an application. There is no distinction between an application-level warning, like an uninitialized instance variable, and a deprecation warning. However, to an engineering team, there's a significant difference.
00:54:34.560 The deprecation worker’s behavior will eventually be removed from the language, making fixing those deprecations much more crucial than resolving an uninitialized instance variable. When we finished our upgrade, I started contemplating if there was a way to enhance warnings in Ruby. Earlier this summer, I collaborated with Aaron Patterson to add an upstream fix to Ruby warnings that exposed a warnings category for each of the warnings.
00:55:31.840 Ruby already had warning categories allowing you to filter out warnings, but this was the opposite behavior we wanted. We wanted to know the kind of warning we had, allowing us to modify application behavior based on that. To achieve this, we exposed existing categories on each of the warnings themselves.
00:56:24.560 If you’re interested, here’s the PR on GitHub. It’s a simple change—only about 54 new characters—but it should make interacting with deprecations in Ruby 3.0 and beyond much more manageable. With this change, we can check the warnings category and adjust behavior based on that in our application. This allows us to raise for any warning categorized as deprecated while maintaining the original behavior for all other warnings.
00:57:32.160 By doing this, we're able to mimic the deprecated behavior of Ruby 3.0 while still running Ruby 2.7. The category feature significantly reduces the effort required to determine the type of warning you're dealing with, which can save time and resources.
00:58:28.480 If all the features, bug fixes, performance enhancements, and future improvements to Ruby aren’t enough to persuade you to update, then hopefully this will: nothing makes an upgrade harder than waiting. The best time to do an upgrade is today. It won't be easy, but it's vital to keep applications secure and functioning smoothly.
00:59:09.520 Additionally, GitHub is running on Ruby 2.7, so you can too! I hope this talk has shown you how upgrades can be low-risk and are always worth the engineering effort to complete them. I hope to see you all next year in person! Thank you, and I hope you enjoy the rest of RubyConf. Thank you to all the organizers for their hard work in making this conference run remotely.
00:59:57.040 I think I have to find questions now, and I'm not sure how to turn this off!
01:00:51.360 I don't want to share my screen anymore—okay, am I still sharing my screen? No? Great! Awesome!
01:01:12.320 So, should I answer the questions that are in the...? Yeah, do you see the questions, the Q&A down there?
01:01:29.440 I could help you. One question that came through is about the process for dual booting. I don't quite understand the question, but the process is that we boot into a different version using the environment variable. Once you set up the environment variable, it compiles with the new Ruby version rather than the old one. You could easily use this with any Docker image; we happen to compile Ruby directly on our systems for reasons related to testing directly from the source.
01:02:14.880 I hope that answers your question. It looks like there was also a follow-up comment that it was regarding what the process looked like. Additionally, you're going to be sharing the slides anywhere? I believe that was a question.
01:02:43.680 Yes, I'll put them on Speaker Deck.
01:03:02.240 Another question is how quickly will GitHub try to move to Ruby 3? Can we have this talk again when you do that?
01:03:29.760 My hope is that someone else at GitHub will manage the upgrade and provide a talk. The team that helps with that work is already on track to build green as of 2.8, so we have some stuff we need to fix and are current on upgrading gems that aren't yet compatible with 3.0. We're aiming to be closer this time since we were about six to eight months behind the release of Ruby 2.7 before we got upgraded in production.
01:04:06.320 Another question was if there were other performance improvements due to frozen string literals, or was it just the boot time that was observed? We implemented frozen string literals over a long period, so we couldn't see the benefits of freezing before being on that version.
01:05:06.040 We made those changes gradually, so we lack concrete metrics from transitioning to the version. Hence, the answer is no; we didn't witness anything concrete from frozen strings, mainly because we didn’t properly measure it.
01:05:39.680 Another question is how did you feel when the positional keyword argument separation was shelved and the deprecation warning was removed?
01:06:18.480 The warning hasn't been removed at all; they've just silenced it. The deprecation warning still exists, and it didn’t shelve it. The compromise was to silence them by default, which means you can deploy without a performance impact. However, you still need to fix them, so it isn’t an excuse to ignore them.
01:07:08.240 It looks like we have one last question. From Keith: I’m a big fan of outputting data in machine-consumable formats from command-line applications. How may that approach have helped you, and how difficult would it be to integrate this into the Ruby interpreter?
01:08:03.440 I don't fully understand how we might utilize that, and I don't really get the question. Keith, if you could clarify in the Slack channel afterward, that would be helpful.
01:08:44.080 I don't see any other questions remaining that we haven’t addressed. Eileen, will you be available in the Slack channel to answer any questions that might pop up?
01:09:46.520 Yes, I’ll be there.
01:10:19.600 All right, stellar! Eileen, thank you very much! You've given talks like this many times before, and I want to honor and appreciate the fact that you're so willing and accessible to do this.
01:10:57.080 Contributions like yours play a major role in simplifying what can be a complex and painful upgrade process for everyone else. Thank you for sharing your experience with us, which has been invaluable.
01:11:51.440 To everyone, jump on over to Slack while we transition to the next talks, which will begin shortly. Thank you, and have a wonderful rest of the day!
Explore all talks recorded at RubyConf 2020
+17