Friday, December 12, 2014

Attribute Caching

Let's talk about caching in C# and making our life easier around it. If you use caching, the most common pattern would be something like:
object GetValue()
{
  object o = GetFromCache("myKey");
  if (o== null)
  {
    o = CalcValue();
    SaveToCache ("myKey", o);
  }
  return o;
}

Pretty simple and obvious, I'm sure you've done it many times. But if you have dozens or hundreds functions like the one above, your code becomes dirty and hard to maintain. You also need to maintain all those unique keys for your cache storage. Wouldn't it be nice if we can write something like:
[Cacheable]
object GetValue()
{
  return CalcValue();
}

Now you can do it exactly like this using AttributeCaching library.

How it Works

CacheableAttribute is translated to C# code behind the scene, which checks if the value is already in cache prior to execute the function. If the value is in cache already then it is returned immediately, and the function body is not even executed. If the value is not in cache yet, then the function is executed, and the value it returns is immediately put into cache. I.e. this is implementation of read-through caching.
As a key to store and read values from cache, the attribute uses a combination of assembly name + method name + passing parameters values. So if you call a function with parameters, a different caching key will be used every time.

Let's consider more complex examples.

Cache Lifetime

If you need your cached values to expire eventually, you can specify different time spans for caching:
[Cacheable (Seconds = 10)]
int CalcEvery10Seconds (int a, int b)
{
  return a+b;
}

[Cacheable (Minutes = 20)]
int CalcEvery20Minutes (int a, int b)
{
  return a+b;
}

[Cacheable (Hours = 1)]
int CalcHourly (int a, int b)
{
  return a+b;
}

[Cacheable (Days = 7)]
int CalcWeekly (int a, int b)
{
  return a+b;
}


Cache Dependencies

It often happens that you need to invalidate cached value immediately, before its time span expires. For example, you save something to database and you want that the cacheable function, which reads the database, returns new values and not the cached ones. Basically you want to purge your cache and ideally just the part required for the function.
To solve this issue you can use dependency tags:
[Cacheable("cars")]
public Car[] GetCars() { ... }

[Cacheable("cars")]
public Car GetCarById (int id) { ... }
Here we tell that GetCars and GetCarById functions depend on "cars" tag.
[EvictCache("cars")]
public void UpdateCar (Car car) { ... }

This example introduces a new attribute EvictCache. When you call UpdateCar() function, it invalidates all cached values with "cars" tag. I.e. after this function is done, the GetCars and GetCarById functions don't have anything in the cache.

More Complex Dependencies

You can also specify multiple dependency tags for a function, and also evict them all all separately. Example:
[Cacheable("cars")]
Car[] GetCars() { ... }

[Cacheable("cars", "cars_jp")]
Car[] GetJapaneseCars() { ... }

[EvictCache("cars", "cars_jp")]
void UpdateCar (Car car) { ... }

In this case UpdateCar will evict everything which has "cars" OR "cars_jp" tags, i.e. cache will be evicted for both GetCars and GetJapaneseCars.

Ignoring Arguments

If cacheable value doesn't depend on all arguments of a cacheable function, you can use CacheIgnore attribute.
[Cacheable]
int Calc(int arg1, [CacheIgnore] int arg2, int arg3) { ... }

Cache Context

Predefined caching rules are really useful, but sometimes we need to break rules. What if you need to change caching duration in runtime? You can access current caching context and manage some rules for that:
[Cacheable (Minutes = 10)]
int Calc(int i)
{
  CacheScope.CurrentContext.LifeSpan = TimeSpan.FromMinutes (1);
  ...
}
In the example you dynamically change cache lifetime from predefined 10 minutes to 1 minute.

Locking

If your cacheable functions is called from several parallel threads at the same time, and it is not cached yet, then you will end up with multiple threads doing the same operation and wasting precious resources. It's a pretty common situation for backend applications: once cache is expired for a heavily using function, avalanche of threads would stuck in that function.
AttributeCaching library prevents parallel execution by default and locks other threads until the already working one is done with that function. I.e. if Thread1 calls F() and shortly after that Thread2 calls F(), Thread2 will be blocked until F() is done in Thread1 and in Thread2 it will be returned the already cached value. What is important is that locking will be done only in the case the function is called with exactly the same arguments.

If for some reason you want to prevent default locking mechanism, use AllowConcurrentCalls property:
[Cacheable (AllowConcurrentCalls = true)]
string Calc(string prop) { ... }

Cache Storage

Some words on what is used to store all caching values. The default storage is MemoryCache from .NET Framework v4. But it can be changed to use Redis as the storage:
//...
CacheFactory.Cache = new RedisCacheAdapter (RedisConnectionString);
//...


No comments:

Post a Comment