Interceptor in the Wild - CodeProject

:

Preface

In this article, you'll find a real world use case for the interceptor pattern. For the sake of simplicity, the code displayed is not the actual production one, but it is strongly based by it.

In this article you won't find the explanaition of what the interceptor pattern consist, neither what Dependency Injection is. It is required that the reader knows already those concepts. For information about the Interceptor Pattern please refer to here.

Introduction

Who never invoked a middleware may throw the first stone here.
I call it a middleware any piece of software that runs anywhere and anyhow, and was made by any. I don't need to know how it's made, just that I need to invoke a service and it gives me an answer, sometimes...

Let's focus on that sometimes.

  • Sometimes, a new version is being deployed and the service is down.
  • Sometimes, there is a bug and the service blows up.
  • Sometimes, the server is so overloaded, the service times out.
  • Sometimes, we lose network and we can't reach the service.
  • Well... there's probably like 1001 reasons why some service fails giving us a proper response.

Can We Do Something About It?

Sure we can. Most of the errors are transient. Ok, we failed the invocation, shall we try again?
This is the right answer. Let's try again, up to a number of tries. If the problem was random and/or caused by a spike, invoking the service more than once can improve us some success rate.
NOTE: Please keep in mind that one of the best practices for distributed systems is to allow message retries without error.

A Naive Example

Let's simulate a faulty service:

public class Service
{
    private static readonly Random Rand = new Random();

    public string GetMyDate(DateTime dateTime)
    {
        //Some work
        Thread.Sleep(2);

        if (Rand.NextDouble() <= .3)
        {
            throw new Exception("Fault!");
        }
        return string.Format("My date is {0}", dateTime);
    }
}

This fake service will randomly blow up 30% of its invocations. This isn't perfect, but it suits our needs. Let's look at how the client will consume this service.

public class NaiveClient
{
    private StatsCounter _counter;

    public NaiveClient(StatsCounter counter)
    {
        _counter = counter;
    }

    public string GetMyDate(DateTime date)
    {
        var faultService = new Service();
        try
        {
            _counter.TotalExecutions++;
            var mydate = faultService.GetMyDate(date);
            _counter.ExecutionSuccess++;
            return mydate;
        }
        catch (Exception)
        {
            _counter.ExecutionError++;
            throw;
        }
    }
}

We're passing a StatsCounter object on the constructor just to get some stats on the end of our demo. This client only invokes the services and keeps track of every execution, and which resulted in success or error.

And finally, here is our test program. Let's simulate 1000 executions. It's really not a big number, but it's enough to get us some data.

class Program
{
    const int TimesToInvoke = 1000;

    static void Main(string[] args)
    {
        var counter = new StatsCounter();
        var client = new NaiveClient(counter);
        counter.Stopwatch.Start();
        for (var i = 0; i < TimesToInvoke; i++)
        {
            try
            {
                client.GetMyDate(DateTime.Today.AddDays(i % 30));
                counter.TotalSuccess++;
            }
            catch (Exception ex)
            {
                counter.TotalError++;
            }
        }
        counter.Stopwatch.Stop();
        counter.PrintStats();
        Console.WriteLine("Press any key to exit");
        Console.ReadKey();
    }
}

In approximately 29 seconds, we invoked 1000 times the Service.GetMyDate and got 321 errors. It’s like 32,1% errors for those who know simple math (for those who don't just take my word for it). That was the expected result.

Try Again, and Again, and Again...

If the error is in fact transient, we can avoid it if we retry the invocation.
The other way around, if the error is caused by missdata that we send erroneously, then retrying won't do us any good. This is where we distinguish application errors or communication errors. For the sake of simplicity, we'll just consider the later one.

Ok, so let's get the party started. We need to repeat the service invocation if we got any exception. One way to do it is to code the repeat logic on the client implementation. But what if we get more services? What if we get more actions to call? No... This is not the way to go.
What if we could put a transparent piece between the code that invokes the client and the client itself? Well, we actually can. It's called an interceptor!

To use an interceptor, first let's call a friend of mine. Dependency Injection!
For the sake of the length of this article, I won't detail what DI is. Just Google it.
Also, for this example, I'll be using NInject and NInject.Extensions.Interception.LinFu packages for handling DI and Interception, but you can easily switch to the DI container of your own choosing. The important thing here is how the pieces work together, not what glues them.

Show Me the Code Already!

Ok, ok.. Let's refactor our NaiveClient, and extract interfaces on the public methods. Then we'll bind the interfaces and the implementations. Oh wait, did I say interfaces?
Right, for the interception to work the best, we need to work with interfaces and not the concrete types themselves. But that is already your common practice, right?

So, here's the glue:

public class Module : NinjectModule
{
    public override void Load()
    {
        Kernel.Bind<StatsCounter>().ToConstant(new StatsCounter());
        Kernel.Bind<INaiveClient>().To<NaiveClient>().Intercept().With<RetryInterceptor>();
    }
}?

What we're telling here is that `INaiveClient` is served by the type `NaiveClient` but has a `RetryInterceptor` between.
Here's the retry:

public class RetryInterceptor : IInterceptor
{
    private readonly StatsCounter _counter;
    private const int Tries = 3;

    public RetryInterceptor(StatsCounter counter)
    {
        _counter = counter;
    }

    public void Intercept(IInvocation invocation)
    {
        var tryNumber = 0;
        do
        {
            try
            {
                invocation.Proceed();
                return;
            }
            catch (Exception ex)
            {
                tryNumber++;
                if (tryNumber == Tries)
                {
                    throw;
                }
            }
        } while (true);
    }
}

In simple terms, it's invoking the invocation.Proceed() until it succeeds or we reach the maximum tries.
The invocation.Proceed() is the magic here. From the docs:

Quote:

Continues the invocation, either by invoking the next interceptor in the chain, or if there are no more interceptors, calling the target method.

We need also change our test program, to use the magic DI.

class Program
{
    const int TimesToInvoke = 1000;

    static void Main(string[] args)
    {
        var kernel = new StandardKernel();
        kernel.Load(new Module());

        var counter = kernel.Get<StatsCounter>();
        var client = kernel.Get<INaiveClient>();
        counter.Stopwatch.Start();
        for (var i = 0; i < TimesToInvoke; i++)
        {
            try
            {
                client.GetMyDate(DateTime.Today.AddDays(i % 30));
                counter.TotalSuccess++;
            }
            catch (Exception ex)
            {
                counter.TotalError++;
            }
        }
        counter.Stopwatch.Stop();

        counter.PrintStats();
        Console.WriteLine("Press any key to exit");
        Console.ReadKey();
    }
}?

Take notice that we only work with INaiveClient. We don’t have any direct reference to the RetryInterceptor. The for cycle is the same, no changes whatsoever.

This time, we completed the 1000 invocations in 50 seconds, but we had only 28 failures. That's 2,8% error rate (again, trust my math). It's a really nicer number than 32,1% error rate from our last example, and we "didn’t change anything" on our client (only extracted an interface out of it, that should already be a practice). We just inserted a middle piece that extends the default behavior. The higher time can be easily justified by retries that were made. More 417 invocations were made to accomplish those retries.
Note also the Execution Fail number is a bit higher. That's also because we're doing more invocations.

Does It Get Better Than That?

This is a great improvement. We traded off some performance to gain some resilience. But can we improve this better, and of course, without changing any implementation?
Of course we can.

Let's face it. If we throw a ball to a solid wall, it will ricochet every time. So why bother to throw it all again, and again, and again.
The same way, most services that output data that isn't changing every second (an employee name, address, status, etc.) can be safely cached. This way, we surely are less error prone to our FaultyService and we should gain performance because we'll use the service less.

Do We Need to Change?

No, we do not. We already made the required changes when we first introduced the interceptor.
Nothing keeps us from adding a new piece between and let that piece decide if we need to really invoke the service or we already know the result and output it right away.

To simulate a cache, I created a simple cache provider.

public interface ICacheProvider
{
    bool TryGet(object key, out object value);
    void Set(object key, object value);
}

I also created a PoorMansCacheProvider that only stores the values on an Hastable. Please do note that this cache isn't production quality. It's just an academic proof of concept for this article.

Here's the cache interceptor:

public class CacheInterceptor : IInterceptor
{
    private readonly ICacheProvider _cacheProvider;

    public CacheInterceptor(ICacheProvider cacheProvider)
    {
        _cacheProvider = cacheProvider;
    }

    public void Intercept(IInvocation invocation)
    {
        var arguments = invocation.Request.Arguments;
        var methodName = invocation.Request.Method.Name;
        // create an identifier for the cache key
        var key = methodName + "_" + string.Join("", arguments.Select(a => a ?? ""));
        object value;
        if (_cacheProvider.TryGet(key, out value))
        {
            invocation.ReturnValue = value;
            return;
        }

        invocation.Proceed();

        _cacheProvider.Set(key, invocation.ReturnValue);
    }
}

On it, I’m just generating a key that allows me to differentiate action invocations (even so, in this example we only got 1) and also differentiate invocations by parameters.
If the key exists, we return the value we got on store. If not, we proceed the chain of execution and save the result. Simple, no?

Setup

Most of the hard work is done, but there is still a detail on the setup of our interceptors.

public class Module : NinjectModule
{
    public override void Load()
    {
        Kernel.Bind<StatsCounter>().ToConstant(new StatsCounter());
        Kernel.Bind<ICacheProvider>().ToConstant(new PoorMansCacheProvider());
        var binding = Kernel.Bind<INaiveClient>().To<NaiveClient>();
        binding.Intercept().With<CacheInterceptor>().InOrder(1);
        binding.Intercept().With<RetryInterceptor>().InOrder(2);
    }
}

Note the `InOrder` extension. That's the way we setup the execution order of the interceptors.

We invoked the service 1000 times in 1.6 seconds. That is a huge performance increase. As for the resulting numbers:
0.15% error rate. We're increasing performance and in the process we increased the resilience. By not needing to invoke the service so many times, we did increase the resilience by a significant order of magnitude.
Notice the Execution Success: 30. This 30 is the different invocations we have. For the most sharp reader, we're invoking the service like this:

client.GetMyDate(DateTime.Today.AddDays(i % 30));

This causes the cache to store 30 different values, hence the 30 success invocations.
To achieve those 30 success, we needed to call the service 45 times.
1 time out of those 1000, we couldn't get any response on any of the tries before the result was cached.

Conclusion

I don't know about you, but this is a really neat way of improving an application performance.
The awesome part of this series is that we didn't have to mess around the client implementation. We extracted it an interface, but that's about it, and it should be already your way to do things. 

Quote:

Program to interfaces, not implementations

Dependency Injection and Interception are indeed tools that you should always have in your pocket.

History

  • 2015-04-08 - Initial version
  • 2015-05-09 - Published and added download source package