Agatha’s Caching Layer Implementation: First Draft
Posted by Davy Brion on December 13th, 2009
Well i got a bit carried away and started implementing the caching layer already… i’d like to go over the details of it in this post, but keep in mind that it’s only a first draft and that this will not be in the upcoming 1.0 release. In fact, i haven’t even tested this yet… i don’t even know if it runs. This is just to get some feedback on the design. Once it works, it’ll need some time to mature and to be tested properly so it’ll be in the 1.1 release. With that out of the way, let’s get started.
First of all, there’s the following attribute that you can put on your Request types:
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class EnableResponseCachingAttribute : Attribute
{
public string Region { get; set; }
public int Hours { get; set; }
public int Minutes { get; set; }
public int Seconds { get; set; }
public TimeSpan Expiration
{
get
{
if (Hours == 0 && Minutes == 0 && Seconds == 0)
{
throw new InvalidOperationException("You need to specific at least an hour value, a minute value or a second value");
}
return new TimeSpan(0, Hours, Minutes, Seconds);
}
}
}
I really want to support both programmatic configuration as well as XML configuration (which would always override any existing programmatic configuration for a particular request) but for now, this attribute is the only way. Also, everything in this caching layer assumes that your requests which enable response caching must override the Equals and GetHashCode() methods correctly (hint: use Resharper for that).
When we configure Agatha, we need to transform the caching metadata into objects of the following class:
public class RequestCacheConfiguration
{
public Type RequestType { get; private set; }
public TimeSpan Expiration { get; private set; }
public string Region { get; private set; }
public RequestCacheConfiguration(Type requestType, TimeSpan expiration, string region)
{
RequestType = requestType;
Expiration = expiration;
Region = region;
}
}
And we also need something that contains all of the caching configuration data:
public class CacheConfiguration
{
private Dictionary<Type, RequestCacheConfiguration> requestCacheConfigurations;
public CacheConfiguration(IEnumerable<Request> knownRequests)
{
BuildMapOfConfigurationsForRequestsThatEnabledResponseCaching(knownRequests);
}
private void BuildMapOfConfigurationsForRequestsThatEnabledResponseCaching(IEnumerable<Request> knownRequests)
{
requestCacheConfigurations = new Dictionary<Type, RequestCacheConfiguration>();
foreach (var request in knownRequests)
{
var requestType = request.GetType();
var attribute = Attribute.GetCustomAttribute(requestType, typeof(EnableResponseCachingAttribute)) as EnableResponseCachingAttribute;
if (attribute != null)
{
requestCacheConfigurations.Add(requestType, new RequestCacheConfiguration(requestType, attribute.Expiration, attribute.Region));
}
}
}
public bool IsCachingEnabledFor(Type requestType)
{
return requestCacheConfigurations.ContainsKey(requestType);
}
public RequestCacheConfiguration GetConfigurationFor(Type requestType)
{
return requestCacheConfigurations[requestType];
}
public IEnumerable<string> GetRegionNames()
{
return new HashSet<string>(requestCacheConfigurations.Values.Where(v => !string.IsNullOrEmpty(v.Region)).Select(v => v.Region));
}
public string GetRegionNameFor(Type requestType)
{
return requestCacheConfigurations[requestType].Region;
}
}
These classes already give us all we need to know (for now, at least) to set up our cache. As mentioned previously, i wanted to use a region-based approach like NHibernate’s 2nd Level Cache. Each region is basically a logical cache (and depending on the caching provider you’ll use, perhaps even a separate physical one) which is separated from the other ones. It gives us the ability to specifically clear a logical cache.
So, we need something which can create a cache for a region:
public interface ICacheProvider
{
ICache BuildCache(string region);
}
And the cache itself would need to expose the following methods:
public interface ICache
{
Response GetCachedResponseFor(Request request);
void Store(Request request, Response response, TimeSpan expiration);
void Clear();
}
So now we can write something that manages the whole caching thing. I couldn’t come up with a good name, so i just named the class CacheManager (i know it sucks, so suggest a better name if you know one):
public interface ICacheManager
{
bool IsCachingEnabledFor(Type requestType);
Response GetCachedResponseFor(Request request);
void StoreInCache(Request request, Response response);
void Clear(string region);
}
public class CacheManager : ICacheManager
{
private const string defaultRegion = "_defaultRegion";
private readonly CacheConfiguration configuration;
private readonly ICacheProvider cacheProvider;
private Dictionary<string, ICache> caches;
public CacheManager(CacheConfiguration configuration, ICacheProvider cacheProvider)
{
this.configuration = configuration;
this.cacheProvider = cacheProvider;
BuildCaches();
}
private void BuildCaches()
{
var regions = configuration.GetRegionNames();
caches = new Dictionary<string, ICache>(regions.Count() + 1);
caches.Add(defaultRegion, cacheProvider.BuildCache(defaultRegion));
foreach (var region in regions)
{
caches.Add(region, cacheProvider.BuildCache(region));
}
}
public bool IsCachingEnabledFor(Type requestType)
{
return configuration.IsCachingEnabledFor(requestType);
}
public Response GetCachedResponseFor(Request request)
{
return caches[configuration.GetRegionNameFor(request.GetType()) ?? defaultRegion].GetCachedResponseFor(request);
}
public void StoreInCache(Request request, Response response)
{
var config = configuration.GetConfigurationFor(request.GetType());
caches[config.Region ?? defaultRegion].Store(request, response, config.Expiration);
}
public void Clear(string region)
{
caches[region].Clear();
}
}
The RequestProcessor’s ugly request-processing-loop (i know i need to clean that up) could then look like this:
foreach (var request in requests)
{
try
{
var cachingIsEnabledForThisRequest = cacheManager.IsCachingEnabledFor(request.GetType());
if (cachingIsEnabledForThisRequest)
{
var cachedResponse = cacheManager.GetCachedResponseFor(request);
if (cachedResponse != null)
{
responses.Add(cachedResponse);
continue;
}
}
using (var handler = (IRequestHandler)IoC.Container.Resolve(GetResponseHandlerTypeFor(request)))
{
try
{
if (!exceptionsPreviouslyOccurred)
{
var response = GetResponseFromHandler(request, handler);
exceptionsPreviouslyOccurred = response.ExceptionType != ExceptionType.None;
responses.Add(response);
if (cachingIsEnabledForThisRequest)
{
cacheManager.StoreInCache(request, response);
}
}
else
{
var response = handler.CreateDefaultResponse();
response.ExceptionType = ExceptionType.EarlierRequestAlreadyFailed;
response.Exception = new ExceptionInfo(new Exception(ExceptionType.EarlierRequestAlreadyFailed.ToString()));
responses.Add(response);
}
}
finally
{
IoC.Container.Release(handler);
}
}
}
catch (Exception e)
{
logger.Error(e);
throw;
}
}
And that’s it, so far. Obviously, we need ICache and ICacheProvider implementations for the various caching libraries that are already available. I already wrote an InMemoryCache and InMemoryCacheProvider to facilitate early experimentation/testing of the caching layer:
public class InMemoryCacheProvider : ICacheProvider
{
public ICache BuildCache(string s)
{
return new InMemoryCache();
}
}
// this class should probably never be used in production, but will be useful for testing
public class InMemoryCache : ICache
{
private readonly object monitor = new object();
private Dictionary<Request, Response> cachedResponses;
private Dictionary<Timer, Request> timersToRequests;
public InMemoryCache()
{
Initialize();
}
private void Initialize()
{
cachedResponses = new Dictionary<Request, Response>();
timersToRequests = new Dictionary<Timer, Request>();
}
public Response GetCachedResponseFor(Request request)
{
lock(monitor)
{
if (!cachedResponses.ContainsKey(request))
{
return null;
}
return cachedResponses[request];
}
}
public void Store(Request request, Response response, TimeSpan expiration)
{
var timer = new Timer(expiration.Milliseconds);
lock (monitor)
{
if (cachedResponses.ContainsKey(request))
{
// another request handler stored a cached response already during the processing of this request…
// the last one wins, so get rid of the previous one, including its timer
cachedResponses.Remove(request);
var otherTimer = timersToRequests.First(keyValuePair => keyValuePair.Value.Equals(request)).Key;
GetRidOfTimer(otherTimer);
timersToRequests.Remove(otherTimer);
}
cachedResponses[request] = response;
timersToRequests[timer] = request;
}
timer.Elapsed += timer_Elapsed;
timer.Start();
}
void timer_Elapsed(object sender, ElapsedEventArgs e)
{
var timer = (Timer)sender;
lock (monitor)
{
// if the Clear method has been called before a timer has elapsed, it won’t be in the timersToRequest dictionary anymore
if (timersToRequests.ContainsKey(timer))
{
var request = timersToRequests[timer];
cachedResponses.Remove(request);
timersToRequests.Remove(timer);
}
}
timer.Dispose();
}
public void Clear()
{
lock(monitor)
{
foreach (var timer in timersToRequests.Keys)
{
GetRidOfTimer(timer);
}
Initialize();
}
}
private void GetRidOfTimer(Timer timer)
{
timer.Elapsed -= timer_Elapsed;
timer.Stop();
timer.Dispose();
}
}
That’s it so far… i probably forgot about a lot of stuff that i really should take into account, but at least it’s something that we can start with. Or to throw away and start over

December 14th, 2009 at 11:35 am
It works, apart from a minor detail : ‘new Timer(expiration.Milliseconds);’ should be ‘new Timer(expiration.TotalMilliseconds);’.
The use of the inner Timer makes it really hard to test though.
‘your requests which enable response caching must override the Equals and GetHashCode() methods correctly’
This might be a source of hard to track down bugs. Maybe Agatha could verify this at configuration time ?
December 14th, 2009 at 11:38 am
@Kilfour
ah, thanks for the timer tip
as for the testability of the timer… i suspect a timer wrapper will make its way into the codebase soon enough
and about overriding Equals and GetHashCode, it should indeed be checked at configuration time
December 14th, 2009 at 1:52 pm
Nice post, I got thinking about the caching aspect after reading a few of your earlier posts. I had an idea about creating a custom behaviour which you could for example use as an attribute to decorate the method. Inside the behaviour, think it is a dispatcher, not sure, but anyway you could use the as keyword to check that the incoming type is of ICacheable or something:
var cacheRequest = request as ICacheable;
if(cacheRequest != null){
return cacheRequest.HandleCaching(); //Or something
}
Anyway, just a thought. I am really enjoying your posts on your Agatha project!! Excellent work and keep it up!!
December 15th, 2009 at 5:09 am
Sounds like the windsor interceptor would work well in this situation.
December 15th, 2009 at 6:49 am
@Marco,
we can’t use a container-specific feature…
December 20th, 2009 at 1:35 am
[...] Agatha’s Caching Layer Implementation: First Draft [...]
December 22nd, 2009 at 11:03 pm
[...] Agatha’s Caching Layer Implementation: First Draft [...]
January 11th, 2010 at 9:05 pm
[...] of the requirements for using Agatha’s Caching Layer is that your request types must override the Equals and the GetHashCode methods. In order to [...]
May 9th, 2010 at 1:09 pm
[...] version of the server side caching layer. Consider this implementation to be experimental and there are most likely plenty of issues [...]