Rails 3.1中的Rails.cache错误 - TypeError:无法使用默认proc转储哈希

时间:2011-06-17 21:06:49

标签: ruby-on-rails ruby ruby-on-rails-3 caching memcached

我遇到了3.1.0.rc4上的Rails.cache方法的问题(ruby 1.9.2p180(2011-02-18修订版30909)[x86_64-darwin10])。该代码在2.3.12(ruby 1.8.7(2011-02-18 patchlevel 334)[i686-linux],MBARI 0x8770,Ruby Enterprise Edition 2011.03)上的相同应用程序中正常工作,但在升级后开始返回错误。我还没有弄明白为什么。

当尝试缓存具有多个范围的对象时,似乎会发生错误。

此外,无论使用多少范围,使用lambdas的任何范围都会失败。

我从这些模式中遇到了失败:

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 =&gt; true选项作为替代方案,但由于Rails.cache.fetch块正在尝试缓存对象,因此无法正常工作。

有什么建议吗?提前谢谢!

4 个答案:

答案 0 :(得分:98)

这可能有点冗长,但我不得不花一些时间使用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方法使用这样的哈希,甚至在@dynamic_methods_hash内缓存哈希; column_methods_hash将通过respond_to?method_missing等公共方法(间接)调用。{/ 1}

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-proc之前使用了Marshal;或者你可能正在使用MemoryStore缓存后端,这只不过是一个大哈希。

使用多个scope-with-lambdas可能最终将Procs存储在AR对象中;我希望lambdas与类(或单例类)而不是对象一起存储,但我没有打扰分析,因为respond_to?method_missing的问题使得{{1问题无关紧要。

你可以做些什么

我认为你已经将错误的东西存储在缓存中并且变得幸运。您可以正确地开始使用Rails缓存(即存储简单生成的数据而不是整个模型),也可以实现scope / marshal_dumpmarshal_load / _dump方法在Marshal中概述。或者,您可以使用其中一个MemoryStore后端,并将自己限制为每个服务器进程一个不同的缓存。


执行摘要

除非您已准备好自己处理编组,或者希望将自己限制在MemoryStore缓存后端,否则不能依赖于在Rails缓存中存储ActiveRecord模型对象。

答案 1 :(得分:5)

多亏了他的出色分析。我现在设法让我的模型序列化:

def marshal_dump
  {}.merge(attributes)
end

def marshal_load stuff
  send :initialize, stuff, :without_protection => true
end

我还有一些&#34;虚拟属性&#34;使用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

答案 2 :(得分: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

答案 3 :(得分:0)

完成更改后,只需删除默认的proc。类似的东西:

your_hash.default = nil # clear the default_proc