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_procs 实例。

【讨论】:

非常感谢您的精彩回复!我认为您的诊断完全正确 - 范围内的参数之一是从哈希中提取值。但是,我认为您的更大观点是应该重写我们的 Rails.cache 实现以避免将模型对象直接存储到缓存中。我们之前确实很幸运,但我们应该以此为契机来清理它。我非常感谢详细的分析 - 它非常有帮助! 这仅仅是 Ruby 版本变化造成的吗? “Programming Ruby”谈到 Ruby Marshal “这种二进制格式有一个主要缺点:如果解释器发生重大变化,marshal 二进制格式也会发生变化,并且旧的转储文件可能不再可加载。”所以你的 1.8 对象我们可能已经烂掉了。所以这应该适用于二进制安全后端。 哦,编组一个对象也会导致对象数据被序列化,如果它包含一个非编组对象将引发异常。所以我认为 Marshall 甚至不能真正被依赖于许多复杂的 ActiveRecord 对象。 有没有办法找出导致错误的属性?我遇到了Marshal.dump Brand.firstMarshal.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 不起作用)

为什么此代码块在Rails.cache.fetch之后执行

如何模拟Rails.cache.fetch的nil案例?

使用 Rails.cache 时 Rspec 测试失败,但如果我执行 binding.pry 则通过

Rails 使用 fetch 进行缓存

如何使控制台中的视图缓存片段过期?