Rails upgrade led to Ruby bug

Evgeniy Demin
2 min readJun 30, 2023

--

Have you ever seen Ruby bugs?

I recently did. Surprisingly, it happened during my Rails upgrade for a giant monolith.

My first impression was confusion.

I have never seen Ruby bugs before. But Ruby is another program that great engineers built. Nevertheless, it still could have some minor issues for edge cases.

So it happened to me, and I would like to share this short story on how I found it.

P.S. If you are considering upgrading the Rails version of your application, I encourage you to read my other short article with valuable advice that could help you avoid many issues I had.

Ruby on Rails logo

In large code bases, times when you might want to change something’s behavior, are generally speaking higher. But in some rare cases, you might even want to monkey patch something only for specific contexts rather than globally. Having these strict requirements in mind, you may recall one of the least used features of Ruby: Refinements.

We used refinements to monkey-patch Kernel#puts method for specific lexical contexts where the function has slightly different behavior.

P.S. Let’s take this case as it is.

In terms of the code, the below is a minimal example.

module Refinement
refine Kernel do
def puts(*args)
# We change the behavior
super(2)
end
end
end

module Environment
using Refinement

puts(1) # => 2
end

The code above works as expected with Rails 5.2.
However, it stops working with Rails 6.1.

How is this possible? How could Rails break Ruby functionality?

It turns out the bug is already there; Rails just exposes it. So the difference is that Rails 6 patches Kernel with two classes via prepend.

 Kernel.ancestors
=> [ActiveSupport::ForkTracker::CoreExtPrivate, ActiveSupport::ForkTracker::CoreExt, Kernel]

And this is the reason that causes this bug to be exposed. Here you have a minimal reproducible example.

module Prepender
end

module Refinement
refine Kernel do
def puts(*args)
super(2)
end
end
end

# The line below affects the behavior of Refinement
#
Kernel.prepend(Prepender)
#
# If it is commented out, then we have an expected output of 2
# Otherwise, refinement is ignored, and output is 1

module Environment
using Refinement

puts(1)
end

The bug has been found in Ruby 2.7.7. However, in Ruby 3, it seems it was fixed. I didn’t try to find a release note, but I feel it was a known case.

Please consider subscribing or adding me in social networks!

And don’t forget to share what you think about the topic. Just write me a message. I would be happy to discuss the topic or anything else with you!

--

--

Evgeniy Demin
Evgeniy Demin

Written by Evgeniy Demin

Ruby & Golang practitioner. Remote expert. Open-source contributor. Beginner blogger. Join my network to grow your expertise.