Skip to content

Using Proteus.Retry

Steve Bohlen edited this page Sep 4, 2016 · 4 revisions

See the ReadMe in the Github Repo for a quick getting-started guide.

Constructing a Retry Instance

The first step in using Proteus.Retry is to create a new instance of the Retry class which you will subsequently use to invoke your own method. There are two available constructors for the Retry class, one zero-arg ctor and another that takes a RetryPolicy instance.

var retry = new Retry();  //zero-arg default ctor

// ...or...

var policy = new RetryPolicy();
var retry = new Retry(policy); //ctor that takes a RetryPolicy instance

If you choose the default zero-arg ctor approach, you will be constructing a Retry instance with a default RetryPolicy. For more detail on the default RetryPolicy, see the Retry Policies section of this Wiki, but the important thing to note is that the default RetryPolicy declares zero (no) retry attempts. This means that invoking your own code with the default RetryPolicy is functionally equivalent to invoking your code directly without Retry adding any value.

If you construct a Retry instance using the default ctor, you should (usually) ensure that you apply a non-default RetryPolicy to the Retry instance before using it to invoke your own code. This can be done using the .Policy property of the Retry class as follows:

var retry = new Retry();

var policy = new RetryPolicy();
//TODO: configure properties of the 'policy' instance here

retry.Policy = policy;

Using Retry

For the sake of this (real-world) example, let's assume that you have the following class defined. MyServiceWrapper contains a single method, .DoWork(...) that makes a remote call over the network to a service to return some value. For the sake of our example, let's assume it uses .NET Remoting (so that we can assume that the type of Exceptions that might need to be retried are related to .NET Remoting), although the same principle applies to any other method that might experience transient failures.

public class MyServiceWrapper
{
    public int DoWork(int input)
    {
        var service = new RemoteService(); //something capable of calling the service
        return service.GetResult(input);
    }
}

Once a Retry instance has been created, using it to perform work is as simple as the following:

//create and configure the RetryPolicy
var policy = new RetryPolicy();
policy.MaxRetries = 4;
policy.MaxRetryDuration = TimeSpan.FromSeconds(10);
policy.RegisterRetriableException<RemotingException>();

//create the Retry and assign the RetryPolicy
var retry = new Retry(policy);

//create an instance of your own class
var myService = new MyServiceWrapper;

//use Retry to invoke the method
var result = retry.Invoke(() => myService.DoWork(200));

//'result' now holds the return value from the myService.DoWork(...) invocation

Understanding Retry Program Flow

Its important to understand that Retry does not inherently attempt to perform retries on e.g., background or other threads (although its entirely supported to invoke the entirety of Retry on other threads; see Working wth Threading and Async Methods for more detail).

When using Retry to invoke your method, no matter how many times your method invocation is retried, one of the following will be the (eventual) result:

Successful Outcomes

  1. The return value of your method is returned (if your method is a Func<T>)
  2. void is returned (if your method is an Action -- i.e., a void-returning method)

Unsuccessful Outcomes

  1. A MaxRetryCountExceededException will be thrown by Retry if unsuccessful before reaching the .MaxRetries value of the governing RetryPolicy
  2. A MaxDurationExpiredException will be thrown by Retry if unsuccessful before the .MaxRetryDuration of the governing RetryPolicy expires
  3. If at any point Retry received an 'unexpected' exception for which the governing RetryPolicy has not been configured to consider 'retriable', retries will immediately cease and this 'unexpected' exception will be rethrown to your calling code.

As you can see from this list of possible outcomes, this design has several impacts on your code:

  • The flow of your program code that calls Retry is blocked during a Retry invocation and will not advance to the next instruction until one of the several above-listed outcomes (either successful or unsuccessful) is reached.
  • Because unsuccessful cases all involve throwing exceptions, your calling code should consider a try...catch strategy to react to any unsuccessful outcomes as shown in the following section.

Exception Handing Invocation Pattern

Following is the common usage pattern of the recommended try...catch structure for interacting with Retry:

//TODO: construct Retry and configure RetryPolicy as needed

try
{
    retry.Invoke(() => yourObject.YourMethod(yourArg1, yourArg2));
}
catch (MaxRetryCountExceededException ex)
{
    //TODO: respond to the case where number of retries wasn't sufficient
}
catch (MaxDurationExpiredException ex)
{
    //TODO: respond to the case where insufficient time was available
}
catch (Exception ex) //or specific type as desired
{
    //TODO: respond to any unexpected exceptions
}

Since the specific failure exceptions thrown by Retry all inherit from a common base class (RetryInvocationFailureException) and catch blocks in .NET are polymorphic, if you are not interested in why an invocation was unsuccessful (e.g., whether it was too many retries or that the time allotted for all retries had expired), its easy to collapse the above structure into a more coarse-grained try...catch block as follows:

//TODO: construct Retry and configure RetryPolicy as needed

try
{
    retry.Invoke(() => yourObject.YourMethod(yourArg1, yourArg2));
}
catch (RetryInvocationFailureException ex)
{
    //TODO: respond to the case where the retry was unsuccessful for *whatever* reason
}
catch (Exception ex) //or specific type as desired
{
    //TODO: respond to any unexpected exceptions
}