Making ActiveSupport::Cache consistent

Lighthouse ticket: #4452 Making ActiveSupport::Cache consistent - Ruby on Rails - rails

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.