Click here to Skip to main content
15,891,788 members
Articles / Programming Languages / C#

Trying Times: Creating a Code Trier

Rate me:
Please Sign up or sign in to vote.
4.88/5 (4 votes)
25 Jul 2014CPOL11 min read 8.5K   7  
Make code more resilient to external contstraints by trying operations

Introduction

Many software developers look for ways to make code that depends on external resources and services that may be unavailable or missing due to network hiccups, and other unforeseen circumstances, more resilient, so that their applications are more highly available and cause less user frustration.

Sometimes, as a developer, you may need your code to fail with an exception if a resource or service is missing and doing so is the right course of action, but, depending on the exception, you may want to try a specific operation either until success or a specific exception, condition or other criteria has passed; perhaps because the resource or service may be restored quickly and your application needn't die a horrible death and cause headaches for both the the user and you the developer.

These types of operations can typically be the one-off type. Generally, the try logic is wrapped around the potentially offending code and that's the end of that. There is absolutely nothing wrong with that, except that if you need this code again, you must write it again and again and again.

This series of articles deals with this issue by demonstrating how to implement and use a Trier: a type of class that allows specific instructions or code fragments to be tried until a specific outcome is reached, whether that outcome be success or ultimately failure. In this Part 1, the Trier will be developed and you will see how it can be used to provide a variety of standard uses, but also extended to include very custom and specific try logic to any code and cover a variety of application needs and circumstances that may arise from unavailable resources or services.

You'll also notice this concept is very similar to the try/catch/finally construct. It's analogous, but serves modified purpose in that the code fragment may be executed more than once to achieve the desired result whereas a try/catch/finally is only executed one time (without the aid of a looping mechanism).

It's neither wise nor useful to attempt trying / retrying every bit of code you have. On the contrary, try operations should only be attempted where absolutely necessary and where desired to provide maximum availability and resilience of your application.

Trying Times: Where to Start?

When first determining what you need to do and include in a class that tries code, its helpful to see a non-abstracted version that demonstrates, at least in part, what problem we are trying to solve.

C#
 private static void TryMyCode()
{
 
    string myFilename = "test.dat";
    string fileResults = null;
    int count = 10;
    try
    {
        while (count > 0)
        {
            try
            {
                fileResults = File.ReadAllText(myFilename);
                break;
            }
            catch(IOException)
            {
                count--;
                if (count == 0)
                {
                    throw;
                }
                Thread.Sleep(1000);
            }
        }
        Console.WriteLine(fileResults);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Cannot read file after 10 tries.");
    }
 
} 

From this example, you can see that all we're really trying to do is read all the text from a file. The rest of this code is the cruft that is required so that the read has maximum resilience from outside influences such as network failures or locking due to other processes. Suppose, for example, one process writes the "test.dat" file, but this example code is contained in another process that's going to read it. The primary process that writes the file might not be finished writing the file yet, but you need to know when it's done. Since the .NET Framework provides no formal methodology for determining when a file is available for reading, or more specifically, when no locks are present preventing a read, then this type of code is appropriate in some cases. However, you don't want to try forever (or maybe you do); perhaps ten attempts is enough with a wait of one second between each successive attempt.

The result here, is technically readable, but it certainly obfuscates the true intent, which is fileResults = File.ReadAllText(myFilename) or "I wan't to read all the text from a file." Moreover, this code suffers from other software development ills in that it is not portable nor reusable. It must be recreated each and every time an operation such as this needs to be executed.

A quick analysis of the code will reveal some key features that might provide a good basis for abstraction: 

  • We need to execute one or more statements and catch all exceptions.
  • We need to repeat those statements if a particular condition has not yet been met.
  • We may need to throw an exception (or the original) if the condition has been met.
  • We may need to ignore some exceptions or some types of exceptions may be considered "success."
  • We may need to wait between tries.

So, how could this code be abstracted to make it more reusable? One possible answer:

c2
 private static void TryMyCode2()
{
    string myFilename = "test.dat";
    try
    {
        string results = TryCode10(() => File.ReadAllText(myFilename));
        Console.WriteLine(results);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Cannot read file after 10 tries.");
    }
}
 
private static T TryCode10<T>(Func<T> tryAction)
{
    int count = 10;
    while (count > 0)
    {
        try
        {
            return tryAction();
        }
        catch (IOException)
        {
            count--;
            if (count == 0)
            {
                throw;
            }
            Thread.Sleep(1000);
        }
    }
    throw new IOException("Cannot read from file.");
}

Notice here, in this case, that we created a static method that will execute the supplied delegate, in this case a Func<T>, ten times before throwing an exception if it still fails.

This code is slightly more reusable. The method that gets the text from the file, the TryMyCode2 method, is much simpler and much easier to understand. However it still suffers from some key shortcomings. First, the counting logic is wrapped up in the TryCode10 method, so to make that truly a reusable method, that count must be added as a parameter; several other key values would need to be added as parameters (which we will examine later) which, in turn, would make the method call more difficult to discern its intent. Second, this code also suffers from another shortcoming: it it does not encompass other requirements for trying code fragments, such as a progressive wait time or other unforeseen circumstances that arise while developing software. Third, it seems to be geared specifically for catching IOException exceptions instead of generic exceptions which might limit this method's usefulness.

Perhaps there is something more readable still: 

C#
private static void TryMyCode3()
{
    string myFilename = "test.dat";
    Results<string> results = Trier.Try(TimeSpan.FromSeconds(1), 10, () => File.ReadAllText(myFilename));
    if (results.Success)
    {
        Console.Write(results.Result);
    }
    else
    {
        Console.WriteLine("Cannot read file after 10 tries");
    }
}  

This is certainly simpler to read and gets us closer to the goal of clean code, e.g. code that is easy to read and maintain and understandable. Notice that our File.ReadAllText(string) method won't cause an exception (at least that's how the code sample is portrayed). Instead, we get a handy property we can query to determine if the method execution was successful or not. Also (not shown) the returned results should have an Exceptions property that would provide the exception(s) that may have been generated as a result of the code execution. 

The Trier

We can develop a Trier in one of two ways. First, we could create an interface, an ITrier if you will, or we can create an abstract class. The first option sounds appealing because now any class can be a "Trier of Code," but as we shall see, the second option of an abstract class, provides much better abstraction and reusability similar in reason as to why the Stream or TextReader classes provided by the .NET Framework are themselves abstract classes and not interfaces. 

C#
 public abstract class Trier
{
 
    protected abstract bool CanTry { get; }
 
    public bool HasTried { get; protected set; }
 
    protected virtual void OnTry(Exception exception) { }
 
    protected virtual bool GetIsBlacklisted(Exception ex)
    {
        return false;
    }
 
    protected virtual bool GetIsIgnored(Exception ex)
    {
        return false;
    }
 
    protected virtual bool ThrowFinalException
    {
        get
        {
            return true;
        }
    }
 
    protected virtual bool ThrowOnBlacklistedException
    {
        get
        {
            return true;
        }
    }
} 

In keeping with our previously mentioned list of must haves for this Trier, we need a property, that a derived Trier can override to determine if it "Can Try" an operation; true if we can try the code again, false if we can't; false would mean that some condition has been met that precludes us from executing the code further.

Since, by necessity, a Trier will provide "state," e.g., counting, time delays, et al., it's imperative that we don't try to reuse an old Trier. Since you can just make a new one, there's no reason to "reset" an existing trier and reuse it; having a property that indicates if the trier has been previously used, i.e., HasTried, is useful here for the code that will eventually use the rules that the Trier defines to try the code it needs to try.

The derived class may also want to know if an operation has failed and another try will occur. This is where the OnTry(Exception) method comes in handy. A derived class may not care if an operation has tried or not. However, the Trier may want to log the attempts to some logging or messaging system and this method provides an excellent opportunity for that course of action.

A trier should have a concept of a Blacklist for exceptions. Those exceptions that are so devastating, unrecoverable or harmful, that it absolutely must not try any more times if they occur; the code just needs to fail; hence the GetIsBlackListed virtual method. This method will allow a derived type to determine if a particular exception occurs during a trying is considered to be Blacklisted.

A trier also needs a Whitelist as well. A whitelist is those exception(s) that can safely be ignored. In fact, a white-listed exception or ignored exception may be considered acceptable as a success for the code being tried and as such you don't want to fail the tried code just because this type of exception has occurred. I've seen this many times over the years in legacy code that I have no control over, but an exception is generated and it's expected and irrelevant.

Next, we want to have a ThrowOnBlacklistedException property. This allows us to stop trying the code and throw that exception that was generated instead. Each trier is responsible for determining the efficacy of trying code based on what exception was generated.

Finally, we have a ThrowFinalException property. If the code fails utterly while trying, this property determines if the final exception that was generated should be thrown, or just return back some results object instance similar to the previous example code. 

How Trying Can It Be?

All this is interesting, but it doesn't provide the method whereby code can be tried using a Trier. For this we need a static Try method located in the abstract Trier class itself. This provides several key benefits. First, since this is an abstract class, and we have both private and protected methods, any static method on an abstract class, has access to those methods for any instance of a derived Trier. This provides some great encapsulation of logic.

Let's examine two static methods that we will add to our Trier abstract class: 

C#
public static Results Try(Trier trier, Action method)
{
    return Try<object>(trier, () => { method(); return null; });
}
 
public static Results<T> Try<T>(Trier trier, Func<T> method)
{
    AssertTrierHasNotBeenTried(trier);
    List<Exception> exceptions = new List<Exception>();
    trier.HasTried = true;
    do
    {
        try
        {
            return new Results<T>(method());
        }
        catch (Exception ex)
        {
            if (trier.GetIsIgnored(ex))
            {
                break;
            }
 
            exceptions.Add(ex);
 
            if (trier.GetIsBlacklistedAndThrow(ex))
                throw;
 
            trier.OnTry(ex);
            if (trier.CanTry)
                continue;
            if (trier.ThrowFinalException)
                throw;
            break;
        }
    } while (true);
    return new Results<T>(exceptions);
}

The first thing that stands out is that these two methods accept a method to try and return some results back to the caller. For brevity, I've left the Results classes out of this article, but you can download the supplied code sample to see them.

The Results and Results<T> classes provide a property bag whereby we can store details about the try operation, such as either success or failure and a list of the exceptions that may have been generated while trying the code and perhaps a value returned from the code fragment. One method is generic to allow for a result object to be returned. We can leverage the .NET Framework's ability to create an inherited generic class from a non-generic class so that a Result property can be added.

This is one of the few cases where global exception handler is appropriate. Generally attempting to catch all exceptions is not appropriate, but here, it's vital so that we can use that exception object to test for various rules that the Trier may have in place so we can act accordingly.

You'll notice we enter an infinite loop and then let the Trier, via the Trier.CanTry property to let us know when we can no longer continue. Also, if an exception is blacklisted or ignored then other actions can occur as well. 

How Many Times Should I Try: The Counted Trier

All of this looks nifty I suppose, but what can you do with this code? The following code snippet defines and demonstrates using a CountedTrier. This code Trier counts the failures and stops when the maximum count has been reached. It also becomes the basis for more advanced Triers. 

C#
 public class CountedTrier : Trier
{
  public CountedTrier(int maximumTries)
  {
    MaximumTries = maximumTries;
  }
 
  /// <summary>
  /// Gets the maximum number of tries to be performed before failing.
  /// </summary>
  public int MaximumTries { get; private set; }
 
  /// <summary>
  /// Gets a count of the number of tries that were made.
  /// </summary>
  public int Count { get; private set; }
 
  protected override bool CanTry
  {
    get
    {
      Count++;
      return Count < MaximumTries;
    }
  }
}
 
 
public class Program
{
 
  public static void Main(string[] args)
  {
    var filename = "results.txt";
    var trier = new CountedTrier(10);
    var fileText = Trier.Try(
      trier, 
      () => 
      {
        return File.ReadAllText(filename);
      }
    );
 
  }
 
}

This code sample is more readable than our original samples of the same kind. Although this still is not as useful as it can be, I think by now you're getting the idea of the potential here.

In our example, we created a CountedTrier class that merely increments a value whenever the Trier.CanTry method is called and returns true if the count is less than the MaximumTries property, which for our example is 10, and false if it has counted one more.

This example will, of course fail, and it doesn't address other concerns, such as waiting a period of time, nor determining which exceptions should be really thrown, and which exceptions should allow it to try again.

Before we wrap up this article, lets look at two more Triers that are included in the code sample. 

Perhaps I Should Wait Before Trying Again: A Delayed Trier

This type of Trier extends the CountedTrier we created previously by allowing us to delay a period of time between successive tries. 

C#
 /// <summary>
/// Allows trying an operation a specified interval over a maximum try count.
/// </summary>
public class DelayedTrier : CountedTrier
{
  public DelayedTrier(TimeSpan interval, int maximumTries)
    : base(maximumTries)
  {
    Interval = interval;
  }
 
  /// <summary>
  /// Gets the interval to wait between each try operation.
  /// </summary>
  public TimeSpan Interval { get; protected set; }
 
  protected override bool CanTry
  {
    get
    {
      bool canTry = base.CanTry;
      if (canTry)
          System.Threading.Thread.Sleep(Interval);
      return canTry;
    }
  }
} 

The next example is a different take on the DelayedTrier. Instead of always waiting a fixed amount of time between each successive try, we will wait a progressive or graduated amount of time. In other words, the amount of time to wait between each successive try will be calculated at each interval. The function used to determine the new interval will be defined by the supplied Delayed delegate. This class even further extends the DelayedTrier to provide this functionality. 

C#
 /// <summary>
/// Allows trying an operation a specified interval with a function that determines a new delay between tries over a maximum number of tries.
/// </summary>
public class ProgressiveTrier : DelayedTrier
{
  private readonly Delayed _newDelay;
 
  public ProgressiveTrier(TimeSpan initialInterval, int maximumTries, Delayed newDelay)
    : base(initialInterval, maximumTries)
  {
    AssertNewDelayNotNull(newDelay);
    _newDelay = newDelay;
  }
 
  protected override bool CanTry
  {
    get
    {
      bool canTry = base.CanTry;
      if (canTry)
      {
        Interval = _newDelay(Interval, Count);
      }
      return canTry;
    }
  }
  private static void AssertNewDelayNotNull(Delayed newDelay)
  {
    if (newDelay == null)
    {
        throw new ArgumentNullException("newDelay");
    }
  }
}
 
/// <summary>
/// A method used to adjust the interval between successive tries.
/// </summary>
/// <param name="interval">The current interval between try operations.</param>
/// <param name="count">The number of tries that have already occured.</param>
/// <returns>A new <see cref="TimeSpan"/> that is the new interval to wait before the next try operation.</returns>
public delegate TimeSpan Delayed(TimeSpan interval, int count);

Usage: 

C#
//Progressive Trier example
//This code should wait a new interval based on the count of the number of tries.
//It should take ~4 minutes and 40 seconds to completely fail.
{
  var filename = "results.txt";
  var trier = new ProgressiveTrier(TimeSpan.FromSeconds(5), 10, (interval, count) => TimeSpan.FromSeconds(5 * count));
  var fileText = Trier.Try(
    trier,
    () =>
    {
      return System.IO.File.ReadAllText(filename);
    }
  );
}

 

Conclusion

As demonstrated, it is possible to simplify complex logic to try code fragments until they either pass or fail. It is also possible to abstract that logic away from the code fragment being tried to a more suitable venue to provide exceptional re-usability and clarity for maintenance.

I welcome comments, suggestions and criticism on this topic. Please feel free to leave me feedback. 

History

  • Version 1.0 - Initial release.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
I am a software engineer, developer, programmer with 20+ years of experience working on various types of systems and design and have built a lot of software from the ground up and maintained a lot of software developed by others.

I truly enjoy working in this field and I'm glad I started this career more than 20 years ago.

I've learned, over the years, that the biggest and toughest bugs to find usually have the simplest solution once found.

Comments and Discussions

 
-- There are no messages in this forum --