Castle DynamicProxy - a dirty trick to call invocation Proceed() multiple times in an interceptor

Print Content | More

This is a typical scenario: you have a remote service (a database a web service...anything) which can have connection problems; obviously you don’t want your application to crash the desired behavior can be to retry the operation for a couple of times and then ask the user what to do (with a message box maybe).

Implementing this in Spring.Net is quite easy, here is some ugly code that build up a skeleton for the feature:

[System.CLSCompliant(false)]
public class ExceptionHandlerDaoAroundAdvice : IMethodInterceptor
{
    
    public object Invoke(IMethodInvocation invocation)
    {
        // endless loop to let the user retry the operation in case of db error
        
        int tries = 0;
        while (true) {
            try {
                tries += 1;
                
                object o = invocation.Proceed();
                return o;
            }
            catch (SqlClient.SqlException ex) {
                HandleDbException(ex, tries);
            }
            catch (NHibernate.ADOException ex) {
                HandleDbException(ex, tries);
            }
        }
    }
    
    // log the error and ask user if they want to retry, if not abort program
    private void HandleDbException(Exception ex, int tries)
    {
        // examine the exception and
        // in case of connection problems
        // use your session or connection manager to close any pending connection, thay are not valid anymore;
        if (tries < 4) {
            // let's wait a bit before retry
            Threading.Thread.Sleep(2000);
        }
        else {
            // todo: call here the service that have to ask the user what to do: wait more and retry,
            // contact the network administrator or close the application for example
        }
    }    
}

to wire it to a proxy generated around your object you can write something like:

ProxyFactory factory = new ProxyFactory(YOUROBJECT);
factory.AddAdvice(new ExceptionHandlerDaoAroundAdvice());
return factory.GetProxy();

But since I already use Castle Windsor as my IoC system and DynamicProxy for the lazy loading with NHibernate, in order to reduce the number of assemblies and external library my application uses, inspired by this great series of articles by Krzysztof Kozmic I decided to swap out Spring.Aop and use Castle’s Dynamic proxies.

The first implementation was more or less a plain translation of the code shown above and surprisingly I got an InvalidOperationException for calling invocation.Proceed() multiple times inside the Intercept() method of the interceptor.

To verify this I setup a simple test case with an adhoc interceptor that called invocation.Proceed() multiple times:

public interface ITestClass
{
    void TestMethod();
}
 
public class TestClass : ITestClass
{
    public void TestMethod()
    {
        Console.WriteLine("Test Method Called");
    }
}
 
public class FailingRetryInterceptor : IInterceptor
{
    #region IInterceptor Members
 
    public void Intercept(IInvocation invocation)
    {
        invocation.Proceed();
 
        // we want to call proceed multiple times to simulate maybe something
        // like retrying a connection to a database or a remote service
        // after having informed the user
        
        // let's try to call the proced again, we expect a big time
        // failure here cause the standard implementation does
        // not allow the proceed method to be called multiple times
 
        invocation.Proceed();
    }
 
    #endregion
}
 
[TestFixture]
public class InterceptorsTest
{
    readonly ProxyGenerator _generator = new ProxyGenerator();
 
    [Test]
    public void TestFailingRetryInterceptor()
    {
        Assert.Throws(
            typeof (InvalidOperationException),
            () =>
                {
                    ITestClass sut =
                        _generator.CreateInterfaceProxyWithTarget<ITestClass>(new TestClass(),
                                                                              new FailingRetryInterceptor());
                    sut.TestMethod();
                });
 
    }
}

If you run this test it passes, which means we get the InvalidOperationException. This operation is perfectly legal in Spring while it’s not allowed in Castle, to investigate the issue I got the source code for DynamicProxy and looked at the implementation of the Proceed() method inside the AbstractInvocation class:

   1: public void Proceed()
   2: {
   3:     if (interceptors == null)
   4:         // not yet fully initialized? probably, an intercepted method is called while we are being deserialized
   5:     {
   6:         InvokeMethodOnTarget();
   7:         return;
   8:     }
   9:  
  10:     execIndex++;
  11:  
  12:     if (execIndex == interceptors.Length)
  13:     {
  14:         InvokeMethodOnTarget();
  15:     }
  16:     else if (execIndex > interceptors.Length)
  17:     {
  18:         string interceptorsCount;
  19:         if (interceptors.Length > 1)
  20:         {
  21:             interceptorsCount = " each one of " + interceptors.Length + " interceptors";
  22:         }
  23:         else
  24:         {
  25:             interceptorsCount = " interceptor";
  26:         }
  27:  
  28:         var message = "This is a DynamicProxy2 error: invocation.Proceed() has been called more times than expected." +
  29:                       "This usually signifies a bug in the calling code. Make sure that" + interceptorsCount +
  30:                       " selected for this method '" + Method + "'" +
  31:                       "calls invocation.Proceed() at most once.";
  32:         throw new InvalidOperationException(message);
  33:     }
  34:     else
  35:     {
  36:         interceptors[execIndex].Intercept(this);
  37:     }
  38: }

Looking at the implementation is pretty easy to understand why we cannot call the proceed more than one time per interceptor: if we do the we increment the execIndex outside the bound of the interceptors array.

In my application I have full control over which interceptors get injected on each proxy, so a simple hack would be to allow me to reset the ‘execIndex’ variable to -1; doing this way the next time you call invocation.Proceed() from an interceptor the whole chain start from the beginning allowing you to recover and retry the operation.

I don’t like to modify the library code, cause at every update I have to reapply the path again, the Hack here is to use some reflection and write an extension method for the IInvocation interface:

public static class IInvocationExtensions
{
    private static FieldInfo _resetInvocationInterceptorsCall;
 
    public static void Reset(this IInvocation invocation)
    {
        if (_resetInvocationInterceptorsCall == null)
        {
            Type invoc = FindBaseType(invocation.GetType(), typeof(AbstractInvocation));
            if (invoc == null)
                throw new InvalidOperationException("IInvocationExtensions - Cannot find AbstractInvocation as base class.");
            _resetInvocationInterceptorsCall = invoc.GetField("execIndex", BindingFlags.Instance |
                                                                                              BindingFlags.NonPublic);
        }
        // reinitialize the index of execution, so when we call Proceed() again
        // the whole chain of interceptors start again from the first element
        _resetInvocationInterceptorsCall.SetValue(invocation, -1);
    }
 
    private static Type FindBaseType(Type src, Type lookingFor)
    {
        while (!(src == typeof(object)) && (src != lookingFor))
        {
            src = src.BaseType;
        }
        if (src == lookingFor)
            return src;
        return null;
    }
}

We can now update our test and write another interceptor that call the newly created Reset() method to allow us to recover and start the execution again:

public class SuccessRetryInterceptor : IInterceptor
{
    #region IInterceptor Members
 
    private bool firstcall = true;
 
    public void Intercept(IInvocation invocation)
    {
        invocation.Proceed();
 
        if (firstcall)
            {
                firstcall = false;
                // we want to call proceed multiple times to simulate maybe something
                // like retrying a connection to a database or a remote service
                // after having informed the user
 
                // we ask to reset the current call and start it again from the beginning of the
                // interceptors chain
                invocation.Reset();
 
                // let's try to call the proced again, we expect a big failure here cause the standard implementation does
                // not allow the proceed method to be called multiple times
                invocation.Proceed();
 
                // note that in this test we used the firstcall variable to stop the recursive calls
                // of this interceptor.
            }
    }
 
    #endregion
}
 
[TestFixture]
public class InterceptorsTest
{
    readonly ProxyGenerator _generator = new ProxyGenerator();
 
    [Test]
    public void TestSuccessRetryInterceptor()
    {
        Assert.DoesNotThrow(() =>
        {
            ITestClass sut = _generator.CreateInterfaceProxyWithTarget<ITestClass>(new TestClass(), new SuccessRetryInterceptor());
            sut.TestMethod();
        });
 
    }
}

If you execute this test you can see that it passes and that the TestMethod() of the test class gets called two times.

CastleInterceptorsProceed

BEWARE! This is a dangerous technique and you have to be absolutely aware of what you are doing, cause you are executing the whole interceptors’ chain from the beginning..so if some of your interceptors modify the data or perform some advanced operations you have to know exactly what they do in order to not alter/invalidate the data of your application.



Castle windsor, Dynamicproxy, Multiple, Proceed

0 comments

Related Post

All fields are required and you must provide valid data in order to be able to comment on this post.


(will not be published)
(es: http://www.mysite.com)