The Inquisitive Coder – Davy Brion's Blog

Trying to walk that thin line between intelligence and ignorance

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 :)

9 Responses to “Agatha’s Caching Layer Implementation: First Draft”

  1. kilfour Says:

    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 ?

  2. Davy Brion Says:

    @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

  3. Andrew Rea Says:

    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!! :-)

  4. Marco Says:

    Sounds like the windsor interceptor would work well in this situation.

  5. Davy Brion Says:

    @Marco,

    we can’t use a container-specific feature…

  6. Interesting Finds: 2009 12.15 ~ 12.20 - gOODiDEA.NET Says:

    [...] Agatha’s Caching Layer Implementation: First Draft [...]

  7. How QuickNet Found 2 Bugs That You And I Didn’t | The Inquisitive Coder – Davy Brion's Blog Says:

    [...] Agatha’s Caching Layer Implementation: First Draft [...]

  8. Checking Whether A Method Is Overridden | The Inquisitive Coder – Davy Brion's Blog Says:

    [...] 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 [...]

  9. Agatha 1.1 Is Out | The Inquisitive Coder – Davy Brion's Blog Says:

    [...] version of the server side caching layer.  Consider this implementation to be experimental and there are most likely plenty of issues [...]

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>