Daniel Hahn

Be sure where your methods are chained to...

I've just figured out a quite obscure bug in our app. It all started like this:


record.freeze.things # record is an ActiveRecord, and "things" is an association on that record.
TypeError: can't modify frozen object
	from (irb):2:in `instance_variable_set'
	from (irb):2

The code above shouldn't crash, because ActiveRecord hast its own #freeze method, which will still allow access to the associations. But our record behaved as if Object#freeze had been called on it. What happend?

It turned out that we had introduced an innocent-looking module:


module Foobar
  extend ActiveSupport::Memoizable

  def other_things
  end
  memoize :other_things
end

and the included it into our ActiveRecord


class Bar < ActiveRecord::Base
  include Foobar
end

What we didn't realize was that the Memoizable module contains some code for freezing objects:


def self.included(base)
  base.class_eval do
    unless base.method_defined?(:freeze_without_memoizable)
      alias_method_chain :freeze, :memoizable
    end
  end
end

def freeze_with_memoizable
  memoize_all unless frozen?
  freeze_without_memoizable
end

When we extended the module with the Memoizable code, we chained the "memoizable freeze" the the #freeze method of the module, which was Object#freeze. Then we injected the module into the Model class, and overwrote the call to the ActiveRecord#freeze method with our method chain.

To fix this, you can work like this:


module Foo

...
  def self.included(base)
    base.class_eval do
      memoize :other_things
    end
  end

  def other_things
  end
end

class Bar < ActiveRecord::Base
  extend ActiveSupport::Memoizable
  include Foo
end

This way the memoization is included into the real ActiveRecord class, and all is fine.

Photo: http://www.flickr.com/photos/leeco/ / CC BY 2.0

This project is maintained by averell23