Monday, January 31, 2011

Simple Caching using Guice

I am at Google now so I decided to use Guice as the framework for my dependency injections needs. I am a big fan of Spring so I was a little concerned that Guice couldn't live up to my expectations. However, after having used it for the last 6 months or so I have to say that I am a fan. Gone are those long spring XML configuration files and replaced with Guice modules.

Recently, while working on my current project we found that some requests against our server were slow. Using my Guice-ified logging framework (maybe another post?) I was able to determine that we were making way too many queries against our persistence layer. Just looking at the queries we could see that a lot of the queries were exactly the same. Immediately, this seems like a problem that could be solved by some caching. Since the main problem was too many persistence store queries per request, I thought that a simple request scoped cache could go a long way and Guice allowed me to do this very easily. The advantage of making this a request scoped cache I really don't need to worry too much about invalidation or thread safety.

The first thing I needed to do was to determine which persistence calls I want to cache. My persistence class is below and I have annotated the cacheable methods with @Cache in addition I have annotated the class with @Cacheable.


@Cacheable
@Singleton
public class {

@Cache(queryType = QueryType.LOOKUP_BY_KEY)
public Object lookup(Key key)
  // Does lookup against persistence store and returns result.
}

public Object save(Object resource)
  // Saves to the persistence store.
}
}

The annotations are defined as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.TYPE})
public @interface Cacheable {
}
 
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD})
public @interface PersistenceContextCache {
  /**
   * Used to give the interceptor a hint on how to cache the
   * result of the intercepted method.
   */
  public enum QueryType {
    FIND_BY_KEY,
    LIST,
    LOOKUP_BY_KEY,
    SEARCH
  }

  QueryType queryType();
}

Because I want to be able easily disable my caching layer and not let my caching logic invade my persistence code, I am using an interceptor to implement my caching logic.

/**
 * Intercepts calls to the persistence tier and checks if we
 * already have the result in the cache. If it is in the cache 
 * then the call to the persistence tier is skipped and the
 * cached result is returned. Otherwise, we call through and  
 * cache the result.
 */
public class CacheInterceptor implements MethodInterceptor {
  /**
   * Provides the cache which is request scoped so we don't have
   * to concern ourselves with thread safety.
   */
  @Inject
  private Provider<Cache<CacheKey, Object>> cacheProvider;
  
  @Override
  public Object invoke(MethodInvocation invocation) 
      throws Throwable {
 
    // Construct the cache key
    Cache cacheAnnotation =
        invocation.getMethod().getAnnotation(
            PersistenceContextCache.class);
    CacheKey cacheKey = getCacheKey(
        cacheAnnotation.queryType(), 
        invocation.getArguments(), 
        invocation.getMethod());
 
    Object result = cacheProvider.get().getElement(cacheKey);
 
    // If the result is null, then we have a cache miss so call 
    // through
    if (null == result) {
      result = invocation.proceed();


      // Now add it to the cache
      cacheProvider.get().addElement(
          cacheKey, result != null ? 
              result : new NullCacheEntry());
    } 
 
    return result instanceof NullCacheEntry ? null : result;
  }
}

Now let's wire things together in a Guice module.

public class CacheModule extends AbstractModule {
 
  @Override
  protected void configure() {
    CacheInterceptor cacheInterceptor = new CacheInterceptor();
    // Intercept any calls to methods annotated with @Cache on
    // a class annotated with @Cacheable.
    bindInterceptor(
        Matchers.annotatedWith(Cacheable.class),
        Matchers.annotatedWith(Cache.class),
        cacheInterceptor);
  }
 
  @Provides
  @RequestScoped
  SoftHashMap<CacheKey, Object> provideCache() {
    return new SoftHaspMap<CacheKey, Object>();
  }
}


Some things to note in the above code:
  • Since my interceptor is essentially a singleton but my cache is request scoped, I need to use a provider which allows me to mix scopes.
  • I did not show my implementation of getCacheKey. However, it should be straight-forward to see how you can come up with a Cache Key based on the data I pass in.
  • I did not include my cache invalidation code but you should be able to see that you could simply add another interceptor to clear the cache on any write.
  • I use a NullCacheEntry object to represent a "not found" resource since a Map does not allow for null values.
  • My cache is actually a SoftHashMap which I do not show the implementation of.  The idea is we want to be safe and not let our cache get too big and cause an OutOfMemoryException so these SoftReferences should be cleaned up if the memory gets low.