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.