Saturday, September 22, 2007

Elegantly Retry Code If There's an Error

Sometimes you want to try to retry code if there's an error. The most applicable situation for this might be when a database is busy or when the network is slow. Here's some sample code:

        public void RetryFiveTimes()

        {

            for (int count = 0; count < 5; count++)

            {

                try

                {

                    CodeThatCouldThrowAnError();

                    break;

                }

                catch (Exception)

                {

                }

            }

        }



This seems like a decent first attempt solution, until you find yourself doing this in multiple places. Plus I just don't like the way this looks at all. Let's try a different approach:

        public void RetryFiveTimes()

        {

            Retry.Times(5).Do(delegate { CodeThatCouldThrowAnError(); });

        }



OK, now I'm happier. Let's take a look at the Retry class:

    public delegate void RetryMethod();

 

    public class Retry

    {

        private int _times;

 

        public static Retry Times(int times)

        {

            Retry retry = new Retry();

            retry._times = times;

            return retry;

        }

 

        public void Do(RetryMethod method)

        {

            for (int count = 0; count < _times; count++)

            {

                try

                {

                    method();

                    break;

                }

                catch (Exception)

                {

                }

            }

        }

    }



This Retry class could probably benefit from some improvements. The first thing that comes into my head is the ability to retry for specific exceptions. So let's improve this code a bit:

    public delegate void RetryMethod();

 

    public class Retry

    {

        private Type _type = null;

        private int _times;

 

        public static Retry Times(int times)

        {

            Retry retry = new Retry();

            retry._times = times;

            return retry;

        }

 

        public Retry When<T>() where T : Exception

        {

            _type = typeof(T);

            return this;

        }

 

        public void Do(RetryMethod method)

        {

            for (int count = 0; count < _times; count++)

            {

                try

                {

                    method();

                    break;

                }

                catch (Exception e)

                {

                    if (_type != null && !_type.IsAssignableFrom(e.GetType()))

                        throw e;

                }

            }

        }

    }



Cool, now we can do this:

        public void RetryFiveTimesWhenTheresATimeoutException()

        {

            Retry.Times(5)

                .When<TimeoutException>()

                .Do(delegate { CodeThatCouldThrowAnError(); });

        }



Of course, we don't always have nice exceptions where the type indicates that it was a timeout error. What if the message of the exception contained the information we needed? We could probably add further facilities for the user to inspect the exception and provide feedback as to whether we should continue. Let's try this:

    public class Retry

    {

        private Predicate<Exception> _shouldRetry;

        private Type _type = null;

        private int _times;

 

        public Retry()

        {

            _shouldRetry = DefaultShouldRetry;

        }

 

        public static Retry Times(int times)

        {

            Retry retry = new Retry();

            retry._times = times;

            return retry;

        }

 

        public Retry When<T>() where T : Exception

        {

            _type = typeof(T);

            return this;

        }

 

        public Retry If(Predicate<Exception> predicate)

        {

            _shouldRetry = predicate;

            return this;

        }

 

        private bool DefaultShouldRetry(Exception e)

        {

            if (_type == null)

                return true;

            if (!_type.IsAssignableFrom(e.GetType()))

                return false;

            return true;

        }

 

        public void Do(RetryMethod method)

        {

            for (int count = 0; count < _times; count++)

            {

                try

                {

                    method();

                    break;

                }

                catch (Exception e)

                {

                    if (!_shouldRetry(e))

                        throw e;

                }

            }

        }

    }



Great, now we can do this type of thing:

        public void RetryFiveTimesWhenTheresAnExceptionWithTimeoutInItsMessage()

        {

            Retry.Times(5)

                .If(delegate(Exception e) { return e.Message.Contains("Timeout"); })

                .Do(delegate { CodeThatCouldThrowAnError(); });

        }



OK, that's good enough for now. We could continue with this forever, but this solution seems pretty flexible. I think this is a pretty good example of a fluent interface as well.

5 comments:

Anonymous said...

Thanks a bunch for this, I was wondering how to do that.

sradack said...

Hey, glad I could help.

Anonymous said...

Very nice!

Anonymous said...

This is great info to know.

Anonymous said...

Sweet.