
Caching
The State of the Art
Doing caching right is far from trivial: it's much more than just storing data in a key-value store, which is what most web platforms offer you.
Prudence's caching mechanism features the following:
- Template-based cache key generation with support for custom plugins. This allows you full flexibility in caching data that varies per external and internal conditions.
- Fully integrated with client-side caching: uses conditional HTTP requests to make sure clients don't download data they already have, while guaranteeing that they will download newer versions of the data. This enhances the user experience (faster responses) while saving you on bandwidth.
- Allows tagging of cache entries, so that whole swaths of the cache can be invalidated at once just by specifying a tag.
- Tiered caching strategies: allows chaining cache backends in sequence, such that faster backends can be placed before slower ones.
- Prudence caches compressed (gzip and DEFLATE) representations separately, allowing you to save precious CPU cycles on the server.
- Not just pages: Prudence caches your web APIs, too, with all the same features mentioned above.
- For page caching, Prudence caches included fragments individually, allowing for fine-grained control over which parts of the page are cached. With smart use of cache key templates, you can optimize page caching to perfection.
But what's really great about Prudence is how easy it is to use these features: in most cases caching is pretty much automatic. When you need to customize, the API is clear and easy to use.
Server-Side Caching
Five of the caching APIs are in the "caching" namespace, and one is in "application".
For template resources, you may call these APIs anywhere on the page, but for manual resources they should be called in handleInit.
caching.duration
Specifies the duration of cache entries in milliseconds. Set this to a greater-than-zero value to enable caching on the current resource. The default is zero, meaning that caching is disabled.
You can set this value to a either a number or a string. For example, "1.5m" is 90000 milliseconds. Note, though, they when you read the value, it will always be numeric (a long integer data type).
Once enabled, every incoming request will have a cache key generated for it based on the cache key template, plus compression information. Prudence will attempt to fetch the cache entry from the cache, and if it's still valid, will display it to the user (this is called a "cache hit"). If there is no cache entry, or it's invalid, Prudence will run the resource as usual (this is called a "cache miss"), and then store a new cache entry via the key.
Compression is handled specially: if the requested compressed cache entry does not exist, then Prudence will attempt to fetch the uncompressed cache entry. If that exists, Prudence would simply compress it and store the compressed version so that compression could be avoided in the future. In the debug headers, this would appear as a "hit;encode" event. Likewise, when storing a new compressed cache entry (during a "miss"), Prudence actually stores both the compressed version as well as the uncompressed version.
See the API documentation for more details.
Remarkably, even a very small cache duration of just a second or two can be immensely beneficial. It will ensure that if you're bombarded with a sudden upsurge of user requests to the resource, your application won't collapse. The cost is often very much worth it: having the "freshness" of your data being delayed by just a few seconds is usually not a big deal.
It's important to remember that caching is not always faster than fully generating the page. Caching backends are generally very fast, but they still introduce overhead. So, just like in any scenario, avoid premature optimization and benchmark your resources to be sure that caching would indeed improve your performance and scalability.
You might think that your invalidation scheme is so perfect that there's no reason to ever have your cache entries expire. Well, think again: without a clear expiration time, your cache would continue growing forever. Finite durations thus allow for a way for the cache to recycle.
caching.tags
Tags are simple strings that you can associate with a resource's cache entries, which can then be used to invalidate all entries belonging to a particular tag. You may add as many tags as you wish:
caching.tags.add('blog') caching.tags.add('news.' + newsDate)
Note that tags are associated with all cache entries based on the resource, whatever their final cache key.
See the API documentation for more details.
caching.keyTemplate
The cache key template is a string with variables delimited in curly brackets that is cast into a cache key per request. The variables are elaborated in the chapter on string interpolation, and are essentially the same as those used for URI templates. However, Prudence also lets you install plugins to support your own specialized template variables. You can debug the cache key template by using the caching debug headers.
See the API documentation for more details.
Prudence's default cache key template is sensible enough for most scenarios: "{ri}|{dn}|{nmt}|{nl}|{ne}". You can change it in your settings.js. Let's break it down:
- The "ri" variable is cast to the entire client URI, while "dn" is the document (file) name.
- It's a convention in Prudence, but not a requirement, to use "|" as a separator of cache key elements, because it's a character that won't be used by most template variables.
- "dn" makes sure that dynamic captures and other server-side redirections would still be cached uniquely: the same URI might reach a different document depending on an external factor.
- "nmt", "nl" and "ne" all make sure that we have a different key per negotiated format.
- Pay special attention to "ne": if you are supporting compression, you will need it in your cache key templates. Because Prudence usually handles compression automatically for you, it's easy to forget this important variable.
The above is a good cache key template, but you may want to modify it. Here are two common reasons:
A common scenario is for a resource to be generated different accordingly to the logged-in user. You would thus want to include a user identifier in the cache key. To do this, you would likely need to write a plugin to interpolate that identifier. You cache key template could then look something like "{ri}|{uid}|{MT}|{L}", where "uid" is handled by your plugin.
You might be including the same fragment in many pages, but the fragment in fact will be mostly identical. In this case, you can optimize by using a shorter cache key, such that the fragment would be cached only once for all inclusions. You would thus not want to use "ri". A simple example would be "{dn}|{MT}|{L}". This can also be used in conjunction with per-user caching: for example, if you want to cache the same fragment per-user, it would be "{dn}|{uid}|{MT}|{L}". Note that fragments are never compressed, so you don't need "{ne}".
Generally, creating the best key template involves a delicate balance between on the one hand making sure that differing data is indeed cached separately, while on the other hand making sure that you're not needlessly caching the same data more than once.
caching.keyTemplatePlugins
This powerful feature allows you to interpolate your own custom values into cache keys. While this does mean that some code will be run for every request, even for cache hits, it gives you the opportunity to write efficient, fast code that is used only for handling caching.
A common scenario requiring a key template plugin is to interpolate a user ID. We'd install it like so:
caching.keyTemplatePlugins.put('uid', '/plugins/session/')
The above means that existence of a "uid" variable in a key template would trigger the invocation of the "/plugins/session/" library to handle it.
Actually, Prudence also allows you to install key template plugins by configuring them in your application's settings.js. In that case, the plugin would be installed for all resources:
app.settings = { ... code: { cacheKeyTemplatePlugins: { uid: '/plugins/session/' } } }
The implementation of the plugin, however, would be the same however we install it. Our plugin would be in "/libraries/plugins/session.js":
document.require('/sincerity/objects/') function handleInterpolation(conversation, variables) { for (var v in variables) { var variable = variables[v] if (variable == 'uid') { var sessionCookie = conversation.getCookie('session') if (Sincerity.Objects.exists(sessionCookie)) { var session = getSession(sessionCookie.value) if (Sincerity.Objects.exists(session)) { conversation.locals.put('uid', session.getUserId()) } } } } } function getSession(sessionId) { ... return session }
Implementation notes:
- The entry point is "handleInterpolation", and accepts as arguments the current conversation as well as an array of the variables to interpolate. Prudence makes sure to optimize these call by gathering into the array only those variables actually used in the key template. Of course, it would not call your plugin at all if the variables are not present. Also, no cache key casting would be done at all if caching.duration is zero. (This makes it very safe to install the plugin for all resources in setting.js, knowing that it would never be called unless necessary.)
- In this implementation, we're assuming that a cookie named "session" is sent with the session ID. We would then have some functionality in getSession to retrieve a session object.
- The interpolation "trick" is to set up our variable as a conversation.local. Because conversation.locals are interpolated as is, we've effectively allowed the cache key template to be correctly cast.
See the API documentation for more details.
In the above example, we are retrieving the session in order to discover the user ID, an operation that could potentially be costly. Consider that if we have a cache miss, then the session might be retrieved again in the implementation of the resource.
It's easy to optimize for this situation by storing the session as a conversation.local, such that it would be available in the resource implementation. We'd modify our above plugin code like so:
if (Sincerity.Objects.exists(session)) { conversation.locals.put('session', session) conversation.locals.put('uid', session.getUserId()) }
Then, in our resource implementation, would could check to see if this value is present:
var session = conversation.locals.get('session') if (!Sincerity.Objects.exists(session)) { var sessionCookie = conversation.cookies.get('session') if (Sincerity.Objects.exists(sessionCookie)) { session = getSession(sessionCookie.value) } }
caching.key
This is a read-only value, meant purely for debugging purposes. By logging or otherwise displaying it, you can see the cache key that Prudence would use for the current resource. Would is the key qualifier here: of course, your code displaying the cache key won't actually be run in the case of a cache hit.
Another way to see the cache key is to enable the caching debug headers.
See the API documentation for more details.
application.cache
You'll most likely want to use this API to invalidate a cache tag:
application.cache.invalidate('blog')
See the API documentation for more details.
Backends
See the configuration chapter for a full guide to configuring your tiered caching backends. Prudence comes with many powerful options.
Client-Side Caching
Many HTTP clients, an in particular web clients, can cache results locally. Actually, HTTP specifies two different caching modes:
- Conditional mode: Here, the client caches the downloaded data, but checks with the server to make sure new data is not available. If new data is not available, then the cached data is used, otherwise it is downloaded. Remarkably, this is done in a single step: the HTTP headers returned from the server provide the necessary information about the freshness of the data, and if there is no need to download, then the client will stop there. The request will end with a 304 "not modified" HTTP status code. Prudence provides you with various tools to optimize conditional HTTP, so that you can be sure to do only the minimal amount of work necessary for the check.
- Offline mode: Here, the client is told explicitly not to check with the server for a certain amount of time. Obviously, this provides the best possible performance and user experience: no network chatter is necessary. But, also obviously, this means that during that period there is no way for your application to push new data to the client. Prudence provides you with tools for using offline mode, but you should use it with care.
Automatic Client-Side Caching
Here's the good news: if you're using server-side caching, then client-side caching in conditional mode is enabled for you automatically, by default, for the cached resources. Moreover, Prudence will compute the expiration times accordingly and specifically per request. For example, if you are caching a particular resource for 5 minutes, and a client tries to access that resource for the first time after 1 minute has passed since the cache entry was stored, then the client will be told to cache the resource for 4 minutes. After those 4 minutes have passed, the client won't need to do a conditional HTTP request: it knows that it would need new data.
Automatic client-side caching applies to both template and manual resources.
If you wish, you may change the default mode from conditional to offline in your routing.js:
app.routes = { ... { type: 'templates', clientCachingMode: 'offline', maxClientCachingDuration: '30m' } }
Note that "maxClientCachingDuration" only has an effect in offline mode: it provides a certain safety cap against too-long cache durations. The default is -1, which means this cap is disabled.
Just make sure you understand the implications of offline mode: you will not be able to push changes to the client for the cached duration. You can also turn off client-side caching by setting "clientCachingMode" to "disabled".
You can add automatic client-side caching to static resources, too.
Manual Client-Side Caching
If you're not using Prudence's automatic caching, you can still benefit from client-side caching by using the APIs.
For conditional mode, you have the option of using modification timestamps and/or tags:
- conversation.modificationTimestamp: The client will compare the modification timestamp it stored with its cached entry to this.
- conversation.tagHttp: The client will compare the tag it stored with its cached entry to this.
For offline mode:
- conversation.maxAge: The client will not contact your server regarding the resource until this number of seconds has passed.
- conversation.expirationTimestamp: Similar to "maxAge", but using an explicit expiration time. Note that if both are set, most clients will treat "maxAge" as superseding "expirationTimestamp". ("maxAge" was introduced in HTTP/1.1, "expirationTimestamp" is from HTTP/1.0.)
Note that though the APIs are very simple, leveraging them is not trivial, and may require you to design your data structures and subsystems with significant thought towards client-side caching.
For example, storing a modification timestamp within a single database entry is simple enough, but what if your final data is actually the result of a complex query using from several entries? You could potentially use the latest modification date of all of them: but, as you can see, calculating it can quickly get complicated and inefficient. Sometimes it might make sense to actually "bubble" modification dates upwards to all affected database entries as soon as you modify them: it would make your save operations heavier, but it could very well be worth it for greater scalability and an improved user experience.
In some cases, calculating a tag might be less costly than keeping track of modification timestamps. For some data structures, tags may even be provided for you as a byproduct of how they work: key hashes, serial IDs, checksums, etc., are all great candidates for tags.
Conditional mode can improve the client experience, but it can improve the server experience, too.
Prudence, using a great feature of the Restlet library, lets you create the conditional HTTP headers and return them to the client without generating the response. Thus, only if the conditional request continues would your response generation code be called. This feature is internally used by Prudence's automatic caching, but you can also use it yourself in manual resources, using the "handleGetInfo" entry point.
Here's an example—not the most efficient one, but it will demonstrate the flow:
function handleGetInfo(conversation) { var id = conversation.locals.get('id') var data = fetchDataFromDatabase(id) conversation.locals.put('data', data) return data.getModificationTimestamp() } function handleGet(conversation) { var data = getData(conversation) conversation.modificationTimestamp = data.getModificationTimestamp() return Sincerity.JSON.to(data) } function getData(conversation) { var data = conversation.locals.get('data') if (!Sincerity.Objects.exists(data)) { var id = conversation.locals.get('id') data = fetchDataFromDatabase(id) } return data } function fetchDataFromDatabase(id) { ... }
As you can see, we're storing the fetched data in a conversation.local, so that if handleGet is called after handleGetInfo, we would not have to access the database twice.
What have we accomplished in this example? Not that much: all we've done is avoided JSON serialization for those conditional requests that stop at handleGetInfo. A worthwhile little optimization, to be sure, but not one with very dramatic effects. It might be more effective in cases in which we had other heavy processing in handleGet that could be avoided.
The handleGetInfo trick really shines when you have a shortcut to accessing the modification date or the tag. Consider as a common example the a way a filesystem works: you can fetch the file modification date with one system API call, without actually opening the file for reading its contents, which would of course be a much costlier operation. Using handleGetInfo with that API would be able to affect a crucial (even necessary!) optimization. Indeed, that's how static resources work internally.
But how would you implement this with a database server? Most database servers don't allow for such shortcuts: sure, you could only fetch the modification date column from a row for handleGetInfo, but it would be inefficient if soon after you would also need to fetch the rest of the columns. It would be more efficient to just fetch the entire row at once, and so you're back to our non-dramatic optimization from before.
What you could do, however, is cache only the modification dates separately in a specialized backend that is much lighter and faster than the database server, for example Hazelcast or memcached. Here's how it might look:
function handleGetInfo(conversation) { var id = conversation.locals.get('id') var modificationTimestamp = fetchTimestampFromCache(id) return Sincerity.Objects.exists(modificationTimestamp) ? modificationTimestamp : null } function handleGet(conversation) { var data = fetchDataFromDatabase(conversation) conversation.modificationTimestamp = data.getModificationTimestamp() storeTimestampInCache(id, data.getModificationTimestamp()) return Sincerity.JSON.to(data) } function fetchDataFromDatabase(id) { ... } function fetchTimestampFromCache(id) { return conversation.distributedGlobals.get('timestamp:' + id) } function storeTimestampInCache(id, timestamp) { conversation.distributedGlobals.put('timestamp:' + id, timestamp) }
As you can see, the optimization won't be effective unless the cache is warm. Thus, to make it truly effective, you would need a special subsystem to warm up the cache in the background…
Welcome to the world of high-volume web! The solutions for massive scalability are rarely trivial. While Prudence can't provide you with automation for every scenario, at least it provides you with the tools on which you can build comprehensive solutions. With careful planning, you can go very far indeed.
In summary, before you go ahead and provide a handleGetInfo entry point for every resource you create, consider:
- It could be that you don't need this optimization. Make sure, first, that you've actually identified a problem with performance or scalability, and that you've traced it to handleGet on this resource.
- It could be that you won't gain anything from this optimization. Caches and other optimizations along the route between your data and your client might already be doing a great job at keeping handleGet as efficient as it could be. If not, improving them could offer far greater benefits overall than a complex handleGetInfo mechanism.
- It could be that you'll even hurt your scalability! The reason is that an efficient handleGetInfo implementation would need some mechanism in place to track of data modification, and this mechanism can introduce overhead into your system that causes it to scale worse than without your handleGetInfo.
See "Scaling Tips" for a thorough discussion of the problem of scalability.
Bypassing the Client-Side Cache
A devilishly useful aspect of client-side caching is that the cache key is the entire URL, including the query. This means that by simply adding a query parameter (which you otherwise ignore in your server-side handling), you can force the client to fetch new data, even when using offline mode caching for that resource.
Of course, you can't use this trick unless you can control the URLs which the client uses. Luckily, this is exactly what you can do in HTTP: see the static resources guide for a comprehensive discussion.
Two Client-Side Caching Strategies
The default Prudence application template is configured with minimal client-side caching, which is suitable for development deployments. However, once you are ready to move your application to production or staging, you will likely want a more robust caching strategy.
We will here present two common strategies, and discuss the pros and cons of each. They are intended as polar opposites, though you may very well choose a strategy somewhere in between.
This is a great strategy if you're not feeling very confident about managing caching in your application logic. Perhaps you have too many different kinds of pages requiring different caching strategies. Perhaps you can't maintain the strict discipline required for more aggressive caching, due to a quickly changing application structure ("agile"?) or third-party constraints.
If you're in that boat, short-term caching is recommended over no caching at all, because it would still offer better performance and scalability. Because caching is short-term, any mistakes you make won't last for very long, and can quickly be fixed.
How short a term depends on two factors: 1) usage patterns for your web site, and 2) the content update frequency. For example, if a user tends to spend about an hour browsing your site, then a one-hour caching duration makes sense: the client would only have a slightly slower page load at the beginning of the visit.
Our strategy would then be to:
- Use conditional caching mode for manual and template resources. (This is the default.) This would guarantee that we can always push changes to users even when cached.
- Use offline caching mode for static resources commonly used in web pages, for 1 hour. Sure, we won't be able to push changes for those resources in that hour, but by the next time the user visits our site, they should be OK. Within that hour, they would get excellent performance.
Here's an example routing.js:
app.routes = { '/*': [ ... 'manual', // clientCachingMode: conditional 'templates', // clientCachingMode: conditional { type: 'cacheControl', mediaTypes: { 'image/*': '1h', 'text/css': '1h', 'application/x-javascript': '1h' }, next: { type: 'less', next: 'static' } } ...
For long-term caching to work, you must have good systems in place for bypassing the cache when necessary:
- For the static resource assets used in your HTML pages, you must be able to change the URLs when the resource contents change. See the discussion in the static resources guide. With such a system in place, you could cache these resources forever!
-
Your HTML template pages can be cached offline, too, but this means two things:
- You are sure that you won't have to push changes to the client very soon. For example, if you allow your home page to be cached offline for an hour, the browser won't have to hit your site at all when returning to the home page! You'd likely still want to set the cache durations for such pages to a short-term time, though, because you would, of course, eventually want to push changes.
- You can cache HTML offline while still allowing yourself an opportunity to push modifications to the client, by using JavaScript (or browser plugins, if you must). For example, even while your home page is cached offline, a JavaScript background routine in it might be pulling data from you and modifying that page accordingly.
Our strategy would then be to:
- Enable offline caching mode for template resources. Per resource, we would have to carefully consider what a reasonable caching duration would be. It should usually be the average length of a user visit to our site, and probably should not exceed 24 hours: at least if the users revisit our page the next day, they'll see our updated changes.
- Set offline caching for static resources commonly used in web pages to the far future. This means that we must assume that we can never push changes for a URL, and thus must change the URLs when the content changes.
Here's an example routing.js:
app.routes = { '/*': [ ... 'manual', // clientCachingMode: conditional { type: 'templates', clientCachingMode: 'offline' }, ... { type: 'cacheControl', mediaTypes: { 'image/*': 'farFuture', 'text/css': 'farFuture', 'application/x-javascript': 'farFuture' }, next: { type: 'less', next: 'static' } }, ...
The Prudence Manual is provided for you under the terms of the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. The complete manual is available for download as a PDF.