Feb 23, 2010

Actions and retrys

If you've read any of my previous posts, you could probably guess that I love Linq and everything that came with it :)

Here's a small sample how easily you can expand functionality with Action and Func delegates without class inheritance or composition by just injection inline code.

What we're trying to do here, is a Retry component, that allows you to repeat a piece of functionality until a time limit or count limit exceeds. For example, using a remote call to a web service can sometimes be a fickle beast, so you might want to try again a few times until the service could come back to life if it's temporarily unavailable.

-==- Usage -==-

List results = null;

// retry maximum of 5 times until deciding that the service is not responding and quit.
Retry.Times(5, () => {
  results = ws.GetResults();
});

// retry until 5 minutes have gone or the service responds and returns a value.
Retry.Timespan(TimeSpan.Minutes(5), () => {
  results = ws.GetResults();
});

-==- Sample code -==-

public static void Timespan(TimeSpan timespan, Action action, /* optional */ Action onError = null) {
 // record start time
 DateTime start = DateTime.Now;

 // execute action as many times as timelimit allows.
 while (DateTime.Now <= start.Add(timespan)) {
   if (Execute(action, onError)) return;
 }

 // Couldn't execute action within the timelimit
 // If user provider an error action, execute it.
 if (onError != null) onError();
}


public static void Times(int retryCount, Action action, /* optional */ Action onError = null) {

 // execute action maximum of [retryCount] times.
 for (int i=0; i < retryCount; i++) {
   if (Execute(action, onError)) return;
 }

 // Couldn't execute action in given number of times.
 // If user provider an error action, execute it.
 if (onError != null) onError();
}

private static bool Execute(Action action, Action onError)
{
   // action is required
   if (action == null) throw new ArgumentNullException("action", "You didn't provide an action to execute.");
   try
   {
       // execute action
       action();
       // action didn't throw an exception; success!
       return true;
   }
   catch (Exception ex)
   {
       // action failed
       return false;
   }
}
 -==- Improvements -==-

Of course, as we could be waiting here for a while, we should implement asynchronous calling of the methods so we can execute something else while we wait.

I'm using Task Parallel Library (TPL for short) functionality of .Net 4.0 for this. Of course, the classic Begin/End async pattern would also work here.

-==- Async usage -==-

// retry maximum of 5 times until deciding that the service is not responding and quit.
var task = Retry.Times<List>(5, () => {
  return ws.GetResults();
});

... do something else here ...

// blocks until retry is done.
List result = task.Result;

And if you (or more likely the user) need to cancel the request while it's on:

var cancelSource = new CancellationTokenSource();

var task = Retry.TimeSpan<List>(TimeSpan.FromMinutes(15), () => return ws.GetResults(), cancelSource.Token);

... do something here ...

cancelSource.Cancel();

// after canceling the requests, using task.Result will throw System.AggregateException.

Notice that we are using different methodology with sync/async methods. Sync-methods rely on local variables and async-methods on the other hand return a value.

-==- Sample code for async -==-

public static Task<T> Times<T>(int retryCount, Func<T> action, CancellationToken cancellationToken, /* optional */ Action onError = null)
{
   Task<T> task = new Task<T>(
       () => {
           // try [retryCount] times
           for (int i=0; i<retryCount; i++) {
            
            // check if user requested cancellation
            if (cancellationToken != null)
            {
                cancellationToken.ThrowIfCancellationRequested();
            }

            // execute action
            try
            {
                return action();
            }
            catch (Exception ex)
            {
                // failed. try again.
            }
        }
        
        // operation failed each time, execute onerror event if given
        if (onError != null)
        {
            onError();
        }
        throw new RetryException("Retry method failed.");
       }, 
       cancellationToken);

   // start executing
   task.Start();
   return task;
}

You can find more on how TPL can be used with classic async-patterns in here.

-==- Code -==-

As always, the sample code (with tests this time) can be found here.

0 comments:

Post a Comment