GoRuCo 2012

Micro Talk: From Zero to API Cache w Grape & MongoDB in 10 minutes

We'll take a Grape API from zero to cache in 10-minutes. This cookbook includes support for ETags, handling relational data, 304s, etc., based on several months of incremental development at Art.sy.

GoRuCo 2012

00:00:15.780 Hello, everyone! I'm here to talk about API caching and Bruce Willis. Why Bruce Willis? Because in the two talks I've seen about API caching, his name was used as an example. I committed it to memory, and now whenever I think of caching, I think of him. So that's what I'm going to use.
00:00:36.460 Let's start with some static content, which is quite straightforward. Imagine you have an HTML page. How do you make it cacheable? You can set some HTTP headers, for instance, a Cache-Control header with 'private, max-age=31536000', indicating it expires one year from now. This is quite effective. When you do something like this, think about caching something publicly. This means a CDN in front of your site can also cache the data. Once it has the data, it won't even go back to your server for 365 days or less. You've effectively expanded your site's capacity to handle billions of users.
00:01:30.220 Now, once you have some dynamic content, the situation becomes more complex. How many of you use Grape to write APIs in this room? Quite a few hands! For those watching this live, Grape is a DSL for building RESTful APIs. It sits on top of Rails, so if you're building a Rails application, it's a great choice. I love Grape; it’s a fantastic project and incredibly simple.
00:01:45.250 Here's an example. We're going to return a count, and we can set some similar headers. However, since this data is completely dynamic, we must instruct the caches in front of us, be it CDNs or browsers, to never cache this data. Instead, we want them to go back and request it from the server each time. To achieve this, set the Cache-Control header to 'private', indicating it’s per user, declare that it expires right now, and ensure revalidation is required. If the server is down, it will fail rather than returning a result from a previous cache session.
00:02:14.979 For some reason, you might have to set an expiry date in 1990; maybe Nirvana was topping the VH1 charts at that time, which is a special date for me. With this setup, the client now holds a piece of data, and you'd want to say, 'Oh, I already have it. Just give it back to me if it hasn't changed since last time.' On the server side, you can set a header that indicates the last modified date. For example, we could say, 'Last modified: Saturday the 23rd.' The client can then request the data, and if it hasn't been modified since that last modified date, the server can respond with a '304 Not Modified' status and a content length of zero.
00:02:46.030 However, there is a limitation since timestamps have a granularity of seconds. This means you may miss counts, especially with a counter, which is problematic. The solution is to set up an ETag, which can be a hash of the data. The client sends the ETag, and if it doesn’t match, you can respond with a '304 Not Modified' status. There are two Rack middlewares that manage this for you: Rack Attack, which sets the ETag, and Rack Cache, which respects it. With this, you're only sending content once. As long as the content remains unchanged, you'll receive a '304 Not Modified' response.
00:03:39.580 Unfortunately, while you might think you're saving bandwidth, you still need to process requests on the server side. In an API, the main operations occur server-side, and if you're outputting nothing, that can be wasteful. What can we do about this? We can utilize cache. In this case, you could use Rails cache or any caching implementation. You can cache results based on the count and invalidate that cache whenever the count changes. However, managing cache keys can be tedious. What if we could derive a generic cache key that we can use everywhere? Here's a possibility: a generic cache key that injects the version of the API, the request path, and request parameters. If I were to order by some parameter, I'd get a different set of results.
00:04:51.260 This setup is effective, and using MD5 hashing, we can generate a corresponding ETag. Unfortunately, in real-world applications, things can get complicated. Relationships between objects might be more complex and cannot always be expressed simply in a route. We may have user data that's dependent on various factors beyond straightforward IDs. Therefore, what we truly need is a way to bind cache dependencies to other parameters.
00:05:40.630 For example, we want a cache that specifies these dependencies and allows us to execute code whenever we miss a cache hit. A good example is role-based access. A simple instance could be where I'm either an admin or a regular user, and I want to partition my cache based on roles. This ensures that everyday users receive one data set while admins get a different one. We must implement restrictions to prevent serving data from the cache when access is denied. For instance, if I am logged in as a user and I try to access data designated for an admin, I want to fail with an access denied rather than return previous cached data.
00:06:48.480 With that said, we must also consider cache invalidation. Invalidations may need to occur at various levels, such as by class or instance. For example, if I have an object bound to multiple widgets and I change one widget, I want to invalidate all related widgets—for if a widget collection includes this one, it’s crucial that we clear the cache.
00:07:45.299 So, how do we combine this with an 'If-None-Match' request? Now, when we serve some data to the client and the client returns with an ETag, we want to determine if the data has been modified since the last retrieval. Ideally, we don’t want to hit the database unnecessarily; we are aiming for efficiency. By checking the ETag, if the data hasn't changed, we can return the cached response without additional processing.
00:08:42.789 Today, we're open-sourcing this solution, and you can implement it seamlessly with Grape and Mongoid, which are the frameworks we use. But it can also be extended to other frameworks easily. This functionality provides server-side caching bound to a variety of possibilities. This project is called Garner, found on GitHub under Artsy, the company I work for, which is great! To garner means to collect things from various places and organize them in a single accessible location, much like a cache. Also, both Garner and Grape start with 'G', making them a good pairing. I highly encourage you to try this gem if you face API caching issues. Our production use has saved us substantial processing on the server, harmonizing the key concept that once the client has received the data, it can efficiently query the server for any changes.