Raven DB: Lessons Learned: Caching Contexts

Caching

When you talk about caching in terms of the the full web application stack, you've typically got the following layers:

  • Browser cache
  • CDN cache
  • Application output cache
  • Data cache

However, in a application leveraging Raven DB, the last layer actually gets split up into two layers.

Some Background

The way Raven DB operates is by having the client generate HTTP requests, which are sent across the wire to the server. Therefor, the same standard caching mechanisms that HTTP provides are present. This means, that if a request is made, and Raven DB thinks the data hasn't changed since the last time you requested that same data, then the server responds with HTTP 304 Not Modified, instructing the raven client to continue to use what it got last time.

using (var session = store.OpenSession())
{
    //if the server doesnt have anything different from the last time this was requested,
    //it wont do any processing, and just return HTTP 304 Not Modified to the client. the
    //client will then use what it got last time.
    var foo = session.Load("foos/123");
}

So the first layer of the data cache, you get for free out of the box with Raven. Fortunately the second layer is available as well, if your application needs it.

Aggressive Data Caching

With Raven DB, it's possible to instruct the client to not even ask the server for data again, thereby skipping the HTTP request, even if it might result in a 304. Here's what that looks like:

using (var session = store.OpenSession())
{
    //set up an aggressive caching context, instructing the server to not
    //make an http request if it made one within the last 5 seconds
    using (session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromMinutes(5)))
    {
        var foo = session.Load("foos/123"); //may or may not make a request
    }
}

Runtime Configuration?

We made mention in a previous blog post about a runtime configuration setup that we've provided our ops team with. Having the ability to control the TTL on the Raven runtime configuration seems like a prime candidate to use with this. We wired up the runtime configuration much the same as the output caching runtime configuration from the other blog post.

Clever

Now, to use output caching, it was a simple line to apply the [ConfiguredOutputCache] attribute to our controller actions. However, with the raven data caching, it's a violation of DRY to have to open an aggressive caching context, and pass in a runtime configuration value everywhere it's needed. So, with that in mind, we came up with an extension method to encapsulate that behavior. We thought this was very clever, but it actually turned out to be quite stupid. Can you spot the problem?

public static class DataCachingExtensions
{
    public static T LoadAndCache(this IDocumentSession session, string id)
    {
        using (session.Advanced.DocumentStore.AggressivelyCacheFor(CacheSettings.RavenAggressiveCachingDurationSeconds))
        {
            return session.Load(id);
        }
    }

    public static IRavenQueryable QueryAndCache(this IDocumentSession session)
    {
        using (session.Advanced.DocumentStore.AggressivelyCacheFor(CacheSettings.RavenAggressiveCachingDurationSeconds))
        {
            return session.Query();
        }
    }
}

... 

session.LoadAndCache("foos/123");

...

session.QueryAndCache().Where(f => f.Bar == "Baz").ToList();

The first extension method is fine, but the 2nd one doesn't do anything at all. Why?

It's because Raven doesn't actually execute the HTTP query until it's evaluated. So since we haven't actually executed the query, and have returned from inside the aggressive caching context, the context was disposed before we ever execute the HTTP query, resulting in no caching.

So after feeling pretty silly, we restructured the extension method to simply return the aggressive caching context, so that the caller can encapsulate the full query including its execution.

public static class DataCachingExtensions
{
    public class NonCachingContext : IDisposable
    {
        public void Dispose() { }
    }

    public static IDisposable GetCachingContext(this IDocumentSession session)
    {
        if(CacheSettings.RavenAggressiveCachingDurationSeconds == 0)
        {
            return new NonCachingContext();
        }

        return session.Advanced.DocumentStore.AggressivelyCacheFor(CacheSettings.RavenAggressiveCachingDurationSeconds);
    }
}

...

using (var session = store.OpenSession())
{
    using (session.GetCachingContext())
    {
        session.Query().Where(f => f.Bar == "Baz").ToList();
    }
}

It's worth pointing out that the caching context, when used with a Query, does not actually cache the items returned, but rather just caches the query/response aspect, so subsequent cache-enabled calls to .Load for items that were returned from a cache-enabled query context will still make a request take place, if they weren't already cached by a .Load call themselves.

Happy coding!

Discuss this post

You're in Easy Mode. If you prefer, you can use XHTML Mode instead.
As a new user, you may notice a few temporary content restrictions. Click here for more info.