Faking Regex-Based Cache Keys in Rails

There are many ways to cache data in a Rails application. (The official Rails Guide explains the different approaches well.) Heroku allows you to easily take advantage of Rails caching by connecting your application to Memcached servers through the Dalli client gem.

This drop in solution is simple, but there's one glaring problem. Memcache does not support expiring cache keys with regex. This means if you need to delete keys beginning with "user-1" but not keys beginning with "user-2", you're out of luck. Or so it seems...

When it comes to deleting the cache, your options are to either empty the whole thing, like this:

Rails.cache.clear

...or delete a specific key, like this:

Rails.cache.delete("my-key")

Not long ago, we worked on a data-heavy application that cached some dynamic endpoints with the user's ID as a namespace. The keys looked like this:

user-1-foo user-2-foo

When a user hit the foo endpoint, we served up the cached JSON, specific to that user, with blazing fast Memcache speed. The problem arrived when we needed to expire the cache for a single user. Our first plan was to just use Rails' built-in regex expiry method:

Rails.cache.delete_matched(/regex/)

Lo and behold, Memcache does not support delete_matched. When you think about it, this makes sense. Memcache's prowess is raw speed, which it attains by limiting interactions to simple key/value writes or deletes. Deleting keys by matching a regex pattern is more complex that those two operations, so Memcache chooses not to support it.

A common pattern to get around this limitation is to namespace keys with an integer:

user-1-memcache-iterator-4-foo user-2-memcache-iterator-4-foo

When you need to expire a given user's cache, simply increment the integer that is attached to that user. All queries going forward will use this new number in their keys. The keys that have the user's old memcache-iterator are essentially deleted, since they'll never again be queried. Now, if we want to flush the cache for just user 1, we'll bump up his memcache-iterator to 5 with this simple method:

class User
  def increment_memcache_iterator
    Rails.cache.write("user-#{self.id}-memcache-iterator", self.memcache_iterator + 1)
  end

  def memcache_iterator
    # fetch the user's memcache key
    # If there isn't one yet, assign it a random integer between 0 and 10
    Rails.cache.fetch("user-#{self.id}-memcache-iterator") { rand(10) }
  end
end

We use another method to build up the cache key for our example "foo" endpoint, ensuring that it always utilizes the users's current memcache-iterator:

class User
  def foo_cache_key
    "user-#{self.id}-memcache-iterator-#{self.memcache_iterator}-foo"
  end
end

Finally, we tell Rails to use this foocachekey method when it needs to grab data from Memcache:

class FooController
  caches_action :foo, :cache_path => Proc.new { |c| current_user.foo_cache_key }
end

This solution is handy because it allows you flush the cache for one user, but keep another user's cache intact. It's also blazing fast since all you're doing is incrementing a cached integer. As a bonus, the moment a user's memcache-iterator is changed, we queue up jobs to populate the cache with the new keys:

class User
  def eager_load_foo
    if Rails.cache.fetch(self.foo_cache_key).nil? # a cache of the current iterator doesn't exist, so create it
      Rails.cache.write(self.foo_cache_key, Foo.to_json)
    end
  end
end

QuickLeft closeicon

Let's Build Your Project

Phone: 303.242.5536
Quick Left HQ
902 Pearl St.
Boulder, CO 80302
Quick Left San Francisco
1285 Folsom St.
San Francisco, CA 94103
Quick Left Portland
524 E. Burnside St.
#410
Portland, OR 97214
Quick Left Denver
Galvanize
1062 Delaware St.
Denver, CO 80204