Errore Rails.cache in Rails 3.1 – TypeError: imansible eseguire il dump di hash con proc predefinito

Ho riscontrato un problema con i metodi Rails.cache su 3.1.0.rc4 (ruby 1.9.2p180 (2011-02-18 revisione 30909) [x86_64-darwin10]). Il codice funziona bene nella stessa applicazione in 2.3.12 (ruby 1.8.7 (2011-02-18 patchlevel 334) [i686-linux], MBARI 0x8770, Ruby Enterprise Edition 2011.03), ma ha iniziato a restituire un errore dopo l’aggiornamento. Non sono ancora riuscito a capire perché.

L’errore sembra verificarsi quando si tenta di memorizzare nella cache oggetti con più di un ambito.

Inoltre, qualsiasi ambito che utilizza lambdas ha esito negativo indipendentemente dal numero di ambiti.

Ho colpito fallimenti da questi modelli:

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 

Questo è l’errore che ricevo:

 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 `' from script/rails:6:in `require' from script/rails:6:in `' 

Ho provato a utilizzare l’opzione: raw => true come alternativa, ma non funziona perché i blocchi Rails.cache.fetch stanno tentando di memorizzare nella cache gli oggetti.

Eventuali suggerimenti? Grazie in anticipo!

Questo potrebbe essere un po ‘prolisso ma ho dovuto passare un po’ di tempo con il codice sorgente di Rails per imparare come funzionano gli interni di caching. Scrivere le cose aiuta la mia comprensione e immagino che condividere alcune note su come funzionano le cose non può far male. Vai alla fine se sei di fretta.


Perché succede?

Questo è il metodo incriminato all’interno di 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 

Notare l’assegnazione a serialized_value . Se fai un giro all’interno di cache.rb , vedrai che usa Marshal per serializzare gli oggetti sulle stringhe di byte prima che cache.rb nella cache e poi Maresciallo di nuovo per deserializzare gli oggetti. Il problema della compressione non è importante qui, l’importante è l’uso del maresciallo.

Il problema è che :

Alcuni oggetti non possono essere scaricati: se gli oggetti da scaricare includono binding, procedure o oggetti metodo, istanze di class IO o oggetti singleton, verrà sollevato un errore TypeError.

Alcune cose hanno uno stato (come i descrittori o blocchi dei file OS) che non possono essere serializzati da Marshal. L’errore che stai notando è questo:

non è ansible scaricare hash con proc predefinito

Quindi qualcuno nel tuo modello ha una variabile di istanza che è un hash e che Hash usa un blocco per fornire valori predefiniti. Il metodo column_methods_hash utilizza un Hash simile e @dynamic_methods_hash cache l’hash all’interno di @dynamic_methods_hash ; column_methods_hash sarà chiamato (indirettamente) da metodi pubblici come respond_to? e method_missing .

Uno di respond_to? o method_missing verrà probabilmente richiamato ogni istanza del modello AR prima o poi e chiamare entrambi i metodi renderà il tuo object non serializzabile. Quindi, le istanze del modello AR sono essenzialmente non serializzabili in Rails 3.

Abbastanza interessante, il respond_to? e le implementazioni method_missing nella 2.3.8 sono anche supportate da un hash che usa un blocco per i valori predefiniti. La cache 2.3.8 è “[…] è pensata per memorizzare le stringhe nella cache”. quindi eri fortunato con un backend che poteva gestire interi oggetti o usava Maresciallo prima che i tuoi oggetti avessero hash-with-procs in loro; o forse stavi usando il backend della cache del MemoryStore e questo è poco più di un grosso Hash.

L’uso di scope-with-lambdas potrebbe finire per archiviare Procs nei tuoi oggetti AR; Mi aspetterei che i lambda siano memorizzati con la class (o la class singleton) piuttosto che con gli oggetti, ma non mi sono preoccupato di un’analisi come il problema con respond_to? e method_missing rende il problema scope irrilevante.

Cosa puoi fare su di esso

Penso che tu stia memorizzando le cose sbagliate nella cache e che tu abbia avuto fortuna. È ansible iniziare a utilizzare correttamente la cache di Rails (ovvero memorizzare semplici dati generati anziché interi modelli) oppure implementare i marshal_dump / marshal_load o _dump / _load come descritto in Marshal . In alternativa, è ansible utilizzare uno dei backend di MemoryStore e limitarsi a una cache distinta per processo del server.


Sintesi

Non si può dipendere dall’archiviazione degli oggetti del modello ActiveRecord nella cache di Rails a meno che non si sia pronti a gestire lo stesso marshalling o si desideri limitarsi ai backend della cache del MemoryStore.

Grazie a mu-is-too-short per la sua eccellente analisi. Sono riuscito a far serializzare il mio modello ora con questo:

 def marshal_dump {}.merge(attributes) end def marshal_load stuff send :initialize, stuff, :without_protection => true end 

Ho anche alcuni “attributi virtuali” impostati da una query di join SQL diretta utilizzando AS ad esempio, i SELECT DISTINCT posts.*, name from authors AS author_name FROM posts INNER JOIN authors ON author.post_id = posts.id WHERE posts.id = 123 . Perché questi funzionino ho bisogno di dichiarare un attr_accessor per ciascuno, quindi scaricare / caricare anche loro, in questo modo:

 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 

Utilizzo di Rails 3.2.18

Mi sono reso conto che l’utilizzo di dove o di un certo ambito ha creato oggetti ActiveRecord::Relation . Poi ho notato che fare un semplice Model.find funzionato. Sospettavo che non gli piacesse l’object ActiveRecord::Relation , quindi ho forzato la conversione in un Array semplice e questo ha funzionato per me.

 Rails.cache.fetch([self.id, 'relA']) do relA.where( attr1: 'some_value' ).order( 'attr2 DESC' ).includes( :rel_1, :rel_2 ).decorate.to_a end 

basta rimuovere il proc predefinito dopo aver finito di modificarlo. qualcosa di simile a:

 your_hash.default = nil # clear the default_proc