Module prepend and alias method in Ruby
Module#prepend allows for powerful meta-programming technique to modify a method with option to call original implementation. It’s especially useful to wrap functions for tracing purposes like this:
class User
def rank!
...
end
end
User.prepend(Module.new do
def rank!
Logger.debug("Calling User#rank!")
super
end
end)
The same can be achieved with alias_method
which copy the original method under a new name:
User.class_eval do
alias_method :rank_without_debug!, :rank!
alias_method :rank!, :rank_with_debug!
def rank!
Logger.debug("Calling User#rank!")
rank_without_debug!
end
end
Unfortunately, when we try to use these two techniques together in a single class, we might end up with a “Stack level too deep” error. It can happen when one of the gems is using Module#prepend
to override methods instrumented by APM (application performance monitoring) tool (in the past, this caused trouble for some of NewRelic’s customers - more info on their blog).
Here is an example for reproducing this case:
module RankTracer1
def self.included(base)
base.alias_method :rank_without_debug!, :rank!
base.alias_method :rank!, :rank_with_debug!
end
def rank_with_debug!
Logger.debug("[RankTracer1] User#rank!")
rank_without_debug!
end
end
module RankTracer2
def rank!
Logger.debug("[RankTracer2] User#rank!")
super
end
end
User.prepend(RankTracer2)
User.include(RankTracer1)
User.new.rank!
# [RankTracer2] User#rank!
# [RankTracer1] User#rank!
# [RankTracer2] User#rank!
# [RankTracer1] User#rank!
# ...
# SystemStackError (stack level too deep)
When we call User#rank!
, the code executes in the following order:
RankTracer2#rank!
as it was prepended toUser
’s ancestors chainRankTracer1#rank_with_debug!
as it was aliased torank!
whenRankTracer1
was included in theUser
classRankTracer#rank!
which callsRankTracer2#rank!
again asalias_method :rank_without_debug!, :rank!
was called afterRankTracer2
was already prepended
As shown above, meta-programming is a powerful mechanism but in the wrong hands (or codebase), it might lead to disaster effects.
Sadly, I’m not aware of any other solution than avoiding mixing these two techniques at the same time (this is also what NewRelic did - they moved from alias_method
to Module#prepend
for instrumenting ActiveRecord
calls).