Create a gist now

Instantly share code, notes, and snippets.

@ahoward /a.rb
Last active Dec 25, 2015

fixing rails' broken caching interface with monkey patches. #YUCK
def write_entry(key, entry, options)
data = Entry.data_for(entry)
expires_at = entry.expires_at.to_i
created_at =
collection.find(_id: key).upsert(_id: key, data: data, expires_at: expires_at, created_at: created_at)
def read_entry(key, options = {})
expires_at =
doc = collection.find(_id: key, expires_at: {'$gt' => expires_at}).first
if doc
data = doc['data'].to_s
value = Marshal.load(data)
created_at = doc['created_at'].to_f
Entry.for(value, created_at)
# this class exists to normalize between rails3 and rails4, but also to
# repair totally broken interfaces in rails - especially in rails3 - that
# result in lots of extra serialization/deserialzation in a class which is
# supposed to be FAST
class Entry < ::ActiveSupport::Cache::Entry
def Entry.is_rails3?
unless defined?(@is_rails3)
@is_rails3 = new(nil).instance_variable_defined?('@value')
# extract marshaled data from a cache entry without doing unnecessary
# marshal round trips. rails3 will have either a nil or pre-marshaled
# @value whereas rails4 will have either a marshaled or un-marshaled @v.
# in both cases we want to avoid calling the silly 'value' accessor
# since this will cause a potential Marshal.load call and require us to
# make a subsequent Marshal.dump call which is SLOOOWWW.
def Entry.data_for(entry)
if is_rails3?
value = entry.instance_variable_get('@value')
marshaled = value.nil? ? Marshal.dump(value) : value
v = entry.instance_variable_get('@v')
marshaled = entry.send('compressed?') ? v : entry.send('compress', v)
end, marshaled.force_encoding('binary'))
# the intializer for rails' default Entry class will go ahead and
# perform and extraneous Marshal.dump on the data we just got from the
# db even though we don't need it here. rails3 has a factory to avoid
# this but rails4 does not so we just build the object we want and
# ensure to avoid any unnecessary calls to Marshal.dump/load... sigh.
def Entry.for(value, created_at)
allocate.tap do |entry|
if is_rails3?
entry.instance_variable_set(:@value, value)
entry.instance_variable_set(:@created_at, created_at)
entry.instance_variable_set(:@compressed, false)
entry.instance_variable_set(:@v, value)
entry.instance_variable_set(:@c, false)
def value
Entry.is_rails3? ? @value : @v
def raw_value
Entry.is_rails3? ? @value : @v


impossible for cache plugin authors to know the state of @value since the object is either contructed via, where @value will be nil or Marshal.dump'd

or maybe the object was created this way

however, the accessor will go boom if the second way is chosen, since it handles only nil or marshaled values

so this is broken, but it's also slow since implementations of 'read_entry' will always cause a Marshal.dump to occur if they don't subvert the contructor with .create - but using .create results in broken entries since #value assumes marshal'd... ;-/


pretty close. since we can call either, :compress => true)

and hit the marshaling code, or not

the issues with this are

  • it's really a terrible signature: having :compress imply marshal is weird
  • it's inverted, we need to know all possible calls to to know if people are inadvertently passing in :compress or not... there is a lot of currying in the cache code so this is definitely possible.


i think the rails3 concept of having two discerte factories is the correct one since, logically, we are either creating an entry and preparing to be written to cache or loading one from cache. so two approaches suggest themselves

add back Entry.create, something like

def Entry.create(object, *args, &block)
  warn 'never pass extract arguments to .create' unless args.empty?

  allocate.tap do |entry|
    entry.instance_eval do
      @v = object
      @c = false

this is ok but the signatures


seem entirely inverted to me. as in .create in ruby-land would normally be saving whereas new would not.

maybe naming would help

  def Entry.for_write(object, created_at, options = {})

  def Entry.for_read(key)

but rails doesn't take this approach much - it seems to think .new is the best...

maybe some sort of callback/hook that's called to compress/marshal entries at the last moment...

  def before_cache
    @v = serialize(@v)


and the end of the day i'd probably take the absolute simplest path: never automagically compress / marshal objects in the base Entry class and, instead, leave that up to Store authors defining 'read_entry' and 'write_entry'. that, however, is a major breaking change and i'm not sure what the rules for rails4 and api signatures are...



🍌 🍌 🍌 🍌 🍌 🍌 🍌 🍌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment