Making ActiveSupport::Cache consistent

Lighthouse ticket: https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4452

I have recently been working on some gems that utilize
ActiveSupport::Cache and ran into some issues with the different
implementations handling the same functionality differently. One of
the issues was that I couldn't rely on expiring entries with
the :expires_in option. MemCacheStore takes this option on a write,
while FileStore takes it on a read, and MemoryStore ignores it all
together so that the cache will just grow until you run out of memory.

I ended up doing a pretty large refactoring of ActiveSupport::Cache to
provide universal support for some options, fix some bugs, and update
the documentation. The patch is attached to this ticket.

Here are the highlights:

All Caches

    * Add default options to initializer that will be sent to all
read, write, fetch, exist?, increment, and decrement
    * Add support for the :expires_in option to fetch and write for
all caches. Cache entries are stored with the create timestamp and a
ttl so that expiration can be handled independently of the
implementation.
    * Add support for a :namespace option. This can be used to set a
global prefix for cache entries.
    * Deprecate expand_cache_key on ActiveSupport::Cache and move it
to ActionController::Caching and ActionDispatch::Http::Cache since the
logic in the method used some Rails specific environment variables and
was only used by ActionPack classes. Not very DRY but there didn't
seem to be a good shared spot and ActiveSupport really shouldn't be
Rails specific.
    * Add support for :race_condition_ttl to fetch. This setting can
prevent race conditions on fetch calls where several processes try to
regenerate a recently expired entry at once.
    * Add support for :compress option to fetch and write which will
compress any data over a configurable threshold.
    * Nil values can now be stored in the cache and are distinct from
cache misses for fetch.
    * Easier API to create new implementations. Just need to implement
the methods read_entry, write_entry, and delete_entry instead of
overwriting existing methods.
    * Since all cache implementations support storing objects, update
the docs to state that ActiveCache::Cache::Store implementations
should store objects. Keys, however, must be strings since some
implementations require that.
    * Increase test coverage.
    * Document methods which are provided as convenience but which may
not be universally available.

MemoryStore

    * MemoryStore can now safely be used as the cache for single
server sites.
    * Make thread safe so that the default cache implementation used
by Rails is thread safe. The overhead is minimal and it is still the
fastest store available.
    * Provide :size initialization option indicating the maximum size
of the cache in memory (defaults to 32Mb).
    * Add prune logic that removes the least recently used cache
entries to keep the cache size from exceeding the max.
    * Deprecated SynchronizedMemoryStore since it isn't needed
anymore.

FileStore

    * Escape key values so they will work as file names on all file
systems, be consistent, and case sensitive
    * Use a hash algorithm to segment the cache into sub directories
so that a large cache doesn't exceed file system limits.
    * FileStore can be slow so implement the LocalCache strategy to
cache reads for the duration of a request.
    * Add cleanup method to keep the disk from filling up with expired
entries.
    * Fix increment and decrement to use file system locks so they are
consistent between processes.

MemCacheStore

    * Support all keys. Previously keys with spaces in them would fail
    * Deprecate CompressedMemCacheStore since it isn't needed anymore

This is a great topic and I just wanted to add that possibly a RedisStore might be good in core too. I was thinking of finding some time next week and doing a ActiveSupport::Cache::RedisStore that passes some tests and conforms to the LocalStore strategy too. I have seen this:

http://github.com/jodosha/redis-store

But it does not look like it does not do exactly all the things the MemCacheStore does. Question, why is compressed memcached store deprecated now?

  • Ken

Moneta already supports Redis (and a bunch of other stores, too).
ActiveSupport's cache could be based on it, rather than duplicating a
lot of work here.

http://github.com/wycats/moneta

I ended up doing a pretty large refactoring of ActiveSupport::Cache to
provide universal support for some options, fix some bugs, and update
the documentation. The patch is attached to this ticket.

First, nice work. Great to see someone really roll up their sleeves.

Here are the highlights:

All Caches

* Add default options to initializer that will be sent to all
read, write, fetch, exist?, increment, and decrement

Sounds good

* Add support for the :expires_in option to fetch and write for
all caches. Cache entries are stored with the create timestamp and a
ttl so that expiration can be handled independently of the
implementation.

This I'm not so sold on, expires in is a memcached implementation
specific feature and adding it to all the other cache stores simply
seems to add overhead for very little gain. No one is seriously going
to be using MemoryStore or FileStore in production and wanting to use
:expires_in. What was it you needed this for?

* Add support for a :namespace option. This can be used to set a
global prefix for cache entries.

Sweet.

* Deprecate expand_cache_key on ActiveSupport::Cache and move it
to ActionController::Caching and ActionDispatch::Http::Cache since the
logic in the method used some Rails specific environment variables and
was only used by ActionPack classes. Not very DRY but there didn't
seem to be a good shared spot and ActiveSupport really shouldn't be
Rails specific.
* Add support for :race_condition_ttl to fetch. This setting can
prevent race conditions on fetch calls where several processes try to
regenerate a recently expired entry at once.

Can you expand on this?

* Add support for :compress option to fetch and write which will
compress any data over a configurable threshold.

How can the read implementation tell whether what it's reading was
:compressed or just happens to look like it might have been?

Everything else looks cool.

This I'm not so sold on, expires in is a memcached implementation
specific feature and adding it to all the other cache stores simply
seems to add overhead for very little gain. No one is seriously going
to be using MemoryStore or FileStore in production and wanting to use
:expires_in. What was it you needed this for?

Mostly what I wanted was for the caches to be consistent. I think
having caches support a time to live on write is a pretty fundamental
feature of a cache because otherwise you are stuck needing to know
what every cache entry is so you can expire it when necessary. For a
small site on a single server, I think MemoryStore or FileStore could
be a perfectly viable option.

> * Add support for :race_condition_ttl to fetch. This setting can
> prevent race conditions on fetch calls where several processes try to
> regenerate a recently expired entry at once.

Can you expand on this?

The logic is that if you specify a race condition ttl, and a process
finds a recently expired entry in the cache during a fetch, it will
first put it back in the cache with an expiration time shortly in the
future. It will then regenerate the entry and store it in the cache.
The advantage is that it can reduce race conditions when a cache entry
expires and you can have potentially dozens of processes trying to
regenerate the entry at the same time. If the process that is
regenerating the entry fails, another process will try again shortly
when the old entry expires again.

Consistency is good - so is backward compatibility - our CloudCache implementation relies upon ActiveSupport::Cache, so prefer that nothing be deprecated, and if so, with a long lead time.

Thanks,

m

getCloudCache.com

Yes. Native file cache let you have caching for a simple site, and
eliminate the need to even have a web server.

+1 on the race ttl too

-- Chad

I updated the patch to remove the deprecation of
ActiveSupport::Cache.expand_cache_key. This method still uses some
Rails specific variables which really isn't ideal. Otherwise the only
deprecations are SynchronizedMemoryStore, CompressedMemCacheStore,
and :expires_in on FileStore#read. The two classes are deprecated
because they don't provide anything beyond what is provided in
MemoryStore and MemCacheStore. FileStore#read deprecates :expires_in
because it can now be set on write.