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.