Rails 3.1 中的 Rails.cache 错误 - TypeError: can't dump hash with default proc
Posted
技术标签:
【中文标题】Rails 3.1 中的 Rails.cache 错误 - TypeError: can\'t dump hash with default proc【英文标题】:Rails.cache error in Rails 3.1 - TypeError: can't dump hash with default procRails 3.1 中的 Rails.cache 错误 - TypeError: can't dump hash with default proc 【发布时间】:2011-09-17 12:34:28 【问题描述】:我在 3.1.0.rc4(ruby 1.9.2p180(2011-02-18 修订版 30909)[x86_64-darwin10])上遇到了 Rails.cache 方法的问题。该代码在 2.3.12(ruby 1.8.7 (2011-02-18 patchlevel 334) [i686-linux],MBARI 0x8770,Ruby Enterprise Edition 2011.03)上的同一应用程序中运行良好,但在升级后开始返回错误。我还没弄明白为什么。
在尝试缓存具有多个作用域的对象时似乎会发生该错误。
此外,任何使用 lambda 的作用域都会失败,无论有多少作用域。
我遇到了这些模式的失败:
Rails.cache.fetch("keyname", :expires_in => 1.minute) do
Model.scope_with_lambda
end
Rails.cache.fetch("keyname", :expires_in => 1.minute) do
Model.scope.scope
end
这是我收到的错误:
TypeError: can't dump hash with default proc
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:627:in `dump'
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:627:in `should_compress?'
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:559:in `initialize'
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:363:in `new'
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:363:in `block in write'
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:520:in `instrument'
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:362:in `write'
from /project/shared/bundled_gems/ruby/1.9.1/gems/activesupport-3.1.0.rc4/lib/active_support/cache.rb:299:in `fetch'
from (irb):62
from /project/shared/bundled_gems/ruby/1.9.1/gems/railties-3.1.0.rc4/lib/rails/commands/console.rb:45:in `start'
from /project/shared/bundled_gems/ruby/1.9.1/gems/railties-3.1.0.rc4/lib/rails/commands/console.rb:8:in `start'
from /project/shared/bundled_gems/ruby/1.9.1/gems/railties-3.1.0.rc4/lib/rails/commands.rb:40:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
我已尝试使用 :raw => true 选项作为替代选项,但这不起作用,因为 Rails.cache.fetch 块正在尝试缓存对象。
有什么建议吗?提前致谢!
【问题讨论】:
为什么要缓存一个范围?缓存实际数据不是更好吗?尝试在每个作用域的末尾添加all
方法。 Model.scope_with_lambda.all
@Oleander - 是的,根据下面的响应,我们之前似乎很幸运能够将模型对象存储到缓存中。我们将重新编码以缓存数据而不是对象。感谢您的想法!
【参考方案1】:
这可能有点冗长,但我不得不花一些时间研究 Rails 源代码来了解缓存内部是如何工作的。把事情写下来有助于我的理解,我认为分享一些关于事情如何运作的笔记不会有坏处。如果您赶时间,请跳到最后。
为什么会发生
这是 ActiveSupport 内部的违规方法:
def should_compress?(value, options)
if options[:compress] && value
unless value.is_a?(Numeric)
compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
serialized_value = value.is_a?(String) ? value : Marshal.dump(value)
return true if serialized_value.size >= compress_threshold
end
end
false
end
注意分配给serialized_value
。如果您在cache.rb
内四处寻找,您会看到它使用Marshal 在对象进入缓存之前将其序列化为字节字符串,然后再次Marshal 以反序列化对象。压缩问题在这里不重要,重要的是 Marshal 的使用。
问题is that:
有些对象不能转储:如果要转储的对象包括绑定、过程或方法对象、类IO实例或单例对象,则会引发TypeError。
有些东西有不能被 Marshal 序列化的状态(例如 OS 文件描述符或块)。您注意到的错误是:
无法使用默认 proc 转储哈希
因此,您的模型中的某个人有一个实例变量,它是一个 Hash,并且该 Hash 使用一个块来提供默认值。 column_methods_hash
方法使用了这样一个Hash,甚至将Hash缓存在@dynamic_methods_hash
里面; column_methods_hash
将被 respond_to?
和 method_missing
等公共方法(间接)调用。
respond_to?
或 method_missing
中的一个可能迟早会在每个 AR 模型实例上被调用,并且调用任一方法会使您的对象无法序列化。因此,AR 模型实例在 Rails 3 中基本上是不可序列化的。
有趣的是,2.3.8 中的 respond_to?
和 method_missing
实现也由使用块作为默认值的 Hash 支持。 2.3.8 的缓存是"[...]is meant for caching strings.",所以你很幸运有一个可以处理整个对象的后端,或者它在你的对象中有 hash-with-procs 之前使用了 Marshal;或者您可能正在使用MemoryStore
缓存后端,这只不过是一个大哈希。
使用多个 scope-with-lambdas 最终可能会将 Procs 存储在您的 AR 对象中;我希望 lambdas 与类(或单例类)而不是对象一起存储,但我没有费心进行分析,因为 respond_to?
和 method_missing
的问题使 scope
问题无关紧要。
你能做些什么
我认为您一直在缓存中存储了错误的内容并且很幸运。您可以开始正确使用 Rails 缓存(即存储简单生成的数据而不是整个模型),或者您可以实现Marshal 中所述的marshal_dump
/marshal_load
或_dump
/_load
方法。或者,您可以使用 MemoryStore 后端之一,并将自己限制为每个服务器进程一个不同的缓存。
执行摘要
您不能依赖将 ActiveRecord 模型对象存储在 Rails 缓存中,除非您准备好自己处理编组或希望将自己限制在 MemoryStore 缓存后端。
问题的确切来源在最近的 Rails 版本中发生了变化,但仍有许多与哈希相关的 default_proc
s 实例。
【讨论】:
非常感谢您的精彩回复!我认为您的诊断完全正确 - 范围内的参数之一是从哈希中提取值。但是,我认为您的更大观点是应该重写我们的 Rails.cache 实现以避免将模型对象直接存储到缓存中。我们之前确实很幸运,但我们应该以此为契机来清理它。我非常感谢详细的分析 - 它非常有帮助! 这仅仅是 Ruby 版本变化造成的吗? “Programming Ruby”谈到 Ruby Marshal “这种二进制格式有一个主要缺点:如果解释器发生重大变化,marshal 二进制格式也会发生变化,并且旧的转储文件可能不再可加载。”所以你的 1.8 对象我们可能已经烂掉了。所以这应该适用于二进制安全后端。 哦,编组一个对象也会导致对象数据被序列化,如果它包含一个非编组对象将引发异常。所以我认为 Marshall 甚至不能真正被依赖于许多复杂的 ActiveRecord 对象。 有没有办法找出导致错误的属性?我遇到了Marshal.dump Brand.first
和Marshal.dump Brand.new
按预期工作的问题,但Marshal.dump FactoryGirl.create(:brand)
触发异常,即使生成的对象是有效的并且看起来与所有其他品牌对象相同。使用FactoryGirl.create
实例化的其他模型实例可以转储,但显然不是所有模型。
仅供参考:我在使用内存缓存时遇到了这个问题。原来内存缓存也编组数据github.com/rails/rails/blob/…【参考方案2】:
感谢 mu-is-too-short 的出色分析。我已经设法让我的模型现在用这个序列化:
def marshal_dump
.merge(attributes)
end
def marshal_load stuff
send :initialize, stuff, :without_protection => true
end
我还使用AS
直接 SQL 连接查询设置了一些“虚拟属性”,例如SELECT DISTINCT posts.*, name from authors AS author_name FROM posts INNER JOIN authors ON author.post_id = posts.id WHERE posts.id = 123
。为了使这些工作,我需要为每个声明一个attr_accessor
,然后也转储/加载它们,如下所示:
VIRTUAL_ATTRIBUTES = [:author_name]
attr_accessor *VIRTUAL_ATTRIBUTES
def marshal_dump
virtual_attributes = Hash[VIRTUAL_ATTRIBUTES.map |col| [col, self.send(col)] ]
.with_indifferent_access.merge(attributes).merge(virtual_attributes)
end
def marshal_load stuff
stuff = stuff.with_indifferent_access
send :initialize, stuff, :without_protection => true
VIRTUAL_ATTRIBUTES.each do |attribute|
self.send("#attribute=", stuff[attribute])
end
end
使用 Rails 3.2.18
【讨论】:
【参考方案3】:我意识到使用 where 或某个范围创建了 ActiveRecord::Relation
对象。然后我注意到做一个简单的Model.find
有效。我怀疑它不喜欢ActiveRecord::Relation
对象,所以我强制转换为普通的Array
,这对我有用。
Rails.cache.fetch([self.id, 'relA']) do
relA.where(
attr1: 'some_value'
).order(
'attr2 DESC'
).includes(
:rel_1,
:rel_2
).decorate.to_a
end
【讨论】:
【参考方案4】:在完成更改后删除默认过程。类似:
your_hash.default = nil # clear the default_proc
【讨论】:
以上是关于Rails 3.1 中的 Rails.cache 错误 - TypeError: can't dump hash with default proc的主要内容,如果未能解决你的问题,请参考以下文章
如何清除 Rails 动作缓存? (Rails.cache.clear 不起作用)