In the last few months, we on the New Relic Ruby agent team have been getting questions about the agent’s support for Module#prepend
usage. However, the agent generally uses alias_method
chains to instrument your code. These two metaprogramming techniques jam up when directly competing against each other—at least, in some setups.
In this post, I will cover what each of these techniques are and discuss when they might come into conflict. I’ll also go over our recommendations for customers encountering this issue and what we’re considering for future releases of the agent in order to resolve it.
Overview of how the agent works
The New Relic Ruby agent is designed to monitor your application and help you identify and solve performance issues. While there are public APIs for custom instrumentation, most customers use the agent’s auto-detected framework and library instrumentation. For example, if the agent detects that you’re using Rails 4 with Puma and Dalli, it automatically installs all the relevant instrumentation. You can see what instrumentation has been installed by tailing the newrelic_agent.log
when starting up your app and grepping for lines that look like INFO : Installing ActiveRecord 4+ middleware instrumentation
.
How does the agent accomplish this magic of automatic instrumentation? By reading through the source code of the libraries we want to support, we can determine commonly used public methods. We then use metaprogramming to modify those methods and insert New Relic instrumentation code. Your app calls and uses these methods without any changes, but behind the scenes the agent can see and instrument those code paths being used, while the library ends up evaluating your call just the same. This has the happy result of no additional work needed by our customers!
What are alias_method
and Module#prepend
?
alias_method
makes a copy of an existing method, but with a new name. Using multiple alias_method
calls is a metaprogramming technique known as “alias_method chaining”, which allows Rubyists to wrap an existing method with new code. In the Ruby agent, we define a “New Relic version” of library methods that include our instrumentation to measure your application’s performance. The New Relic version still ultimately calls the original method, thanks to this alias_method
chaining technique. For example, the agent defines a log_with_newrelic_instrumentation
method, which collects data and then calls a method referenced as log_without_newrelic_instrumentation
. It then opens up ActiveRecord::ConnectionAdapters::AbstractAdapter
and includes a New Relic module that adds in these lines:
alias_method :log_without_newrelic_instrumentation, :log
alias_method :log, :log_with_newrelic_instrumentation
With this chaining, any time your app calls this log
method, it actually ends up calling log_with_newrelic_instrumentation
, which in turn eventually calls log_without_newrelic_instrumentation
, a copy of the original log
method.
Using alias_method
has been a standard practice in the Ruby community for many years to insert some extra code around an existing method. You can see it being used throughout Rails, for example. However, one downside is that it can be difficult to determine where this patching has been done, or who was responsible, if multiple players are metaprogramming. This is why the New Relic Ruby agent always tries to install itself last, after any other libraries that might be involved.
Module#prepend
was introduced a few years ago in Ruby 2.0. Before having the Module#prepend
option, you could only add module methods into a class by including the module into that class. When a module is included in a class, Ruby looks up method calls in the class definition first, then in each included module, and then continues up the inheritance chain through the class’ ancestors.
class A < B
include C
end
thing = A.new
thing.mysterious_method
Order of looking for where mysterious_method
is defined:
Module#prepend
allows you to insert that module below the class in the ancestors chain so that any method calls will first look within the module for a definition. You can then call super
to execute the original method.
class A < B
prepend C
end
thing = A.new
thing.mysterious_method
Order of looking for where mysterious_method
is defined:
When do these techniques conflict?
For alias_method
and Module#prepend
, you could certainly use both techniques in the same project, but generally not on the same methods. If you used both techniques to modify a particular method, such as prepending a module with a new definition for that method, but then included a different module that also has a new definition for that method via alias_method
, you can end up triggering stack level too deep
errors. Here’s how to reproduce that situation:
class Muffin
def batter
'muffin'
end
end
module Blueberry
def batter
"blueberry #{super}"
end
end
module Streusel
def self.included base
base.class_eval do
alias_method :batter_without_topping, :batter
alias_method :batter, :batter_with_topping
end
end
def batter_with_topping
"#{batter_without_topping} with streusel topping"
end
end
Muffin.prepend Blueberry
Muffin.include Streusel
puts Muffin.new.batter
If you try to run this, you will get a stack level too deep
error. When you call the batter
method on a new Muffin
instance, here’s what Ruby does:
1. Looks for a batter
method first in Blueberry
, which it finds and runs.
2. The Blueberry
version of batter
includes a call to super
, which gets Ruby to look for the batter
in the original Muffin
class.
3. However, including Streusel
means that calling batter
on a Muffin
instance actually calls batter_with_topping
from Streusel
.
4. Ruby runs batter_with_topping
, which includes a call to batter_without_topping
... but this goes back to step #1, calling the batter
method from Blueberry
. This is because Streusel
was included last, so by the time the line alias_method :batter_without_topping, :batter
was run, :batter
was already pointing to the one on Blueberry
, not the batter
in Muffin
.
How horrible to be kept from delicious muffins due to this infinite recursive loop!
If instead you first include Streusel
and then prepend Blueberry
, you’d receive blueberry muffin with streusel topping
as your output. You would end up with the same Muffin.ancestors
: [Blueberry, Muffin, Streusel, Object, Kernel, BasicObject]
, but you wouldn’t have methods calling themselves without terminating. This solves the stack level too deep
error, but goes against how the Ruby agent is currently set to install its instrumentation last.
To play around with the different variations of metaprogramming here, see this gist written up by my teammate Kenichi Nakamura.
What should customers do currently?
So, we now know that this problem comes up if your app uses something that does Module#prepend
on a method that the Ruby agent later puts an alias_method
on. For example, this could be a gem that patches ActiveRecord methods that New Relic later does alias_method
on.
In the case where it’s metaprogramming involving ActiveRecord 5, good news—the most recent version of the agent changes our ActiveRecord 5 instrumentation to use Module#prepend
rather than alias_method
! So if this is your situation, the next step would be to upgrade the version of the agent you’re using to 3.17.2.
Outside of ActiveRecord 5, if you run into this issue in your app, we recommend taking a look at how and in what order your gems are being installed. One common troubleshooting technique we use is to set everything up in a barebones test app and slowly add pieces to it until we have the simplest repro case possible. We’ve found that, at least occasionally, the way in which your gems are enabled can have an impact, and when fixed can help everyone get along after all.
The happy situations for wrapping the same method are:
- only
Module#prepend
- only
alias_method
- have
Module#prepend
happen afteralias_method
To achieve either of the first two situations, you could try to determine which particular instrumentation path this is happening on, then see if it would be possible to disable that particular path in one of the conflicting gems. The Ruby agent configuration documentation lists the keys for disabling installation of particular instrumentations.
Rather than having to make that choice, though, you could also try to ensure that the Module#prepend
happens after alias_method
after all. Once you’ve determined the gem that’s using Module#prepend
, you can add :require => false
so that your Gemfile looks like:
gem 'other_gem', :require => false
gem 'newrelic_rpm'
Then, add an after_initialize
block to your app that manually requires that gem, so that any Module#prepend actions are taken after alias_method
:
config.after_initialize do
require 'other_gem'
end
Some possible paths forward under consideration
We’ve been hearing from more customers that are running into this challenge, and we definitely want to resolve it! The core of the issue is that alias_method
and Module#prepend
are examples of two different metaprogramming techniques that work together only in particular situations. If we were to convert the Ruby agent over to using Module#prepend
entirely, it would end up being unusable on other stacks relying on alias_method
.
We ultimately need an agent that is capable of installing instrumentation with one method or the other. This could be configurable, or it could attempt to auto-detect based on what we know about particular libraries. We would want to target the most popular Module#prepend
-using libraries for our customers. If this is something that you anticipate needing, please do let us know by posting in the “Feature Ideas: Ruby Agent” category of the New Relic Online Technical Community.
The views expressed on this blog are those of the author and do not necessarily reflect the views of New Relic. Any solutions offered by the author are environment-specific and not part of the commercial solutions or support offered by New Relic. Please join us exclusively at the Explorers Hub (discuss.newrelic.com) for questions and support related to this blog post. This blog may contain links to content on third-party sites. By providing such links, New Relic does not adopt, guarantee, approve or endorse the information, views or products available on such sites.