Retryer

Summary

Purpose

Automatically retries operations whose failure is expected under certain defined conditions. This pattern enables fault-tolerance for data access operations.

Scenario

When database operations fail, they fail because of conditions outside the application's control. These errors should not cause the application to terminate if it can reasonably recover from then. Client code should analyze the severity of the errors, recover from them as much as possible, and potentially retry operations that failed as a result.

Error recovery, analysis and retry operations is not complex, but it does benefit from a structured approach. Otherwise, this logic may clutter the application code making it more difficult to modify. The Retryer pattern describes a generic structure and logic for automatically retrying data access operations expected to fail under certain conditions, This pattern defines a clear separation between retry logic and retryable database operations.

Structure & UML

The following figure illustrates the static structure for the Retryer:

IRetryable collects data access operations that are potentially retryable. ConcreteRetryable implements this interface to define logic for each retryable operation. IRetryable.Attempt contains logic to perform the data access operation and returns true if this operation was successful, or false if an expected error occurs, or throws an exception if an unexpected error occurs. IRetryable.Attempt contains recovery logic and is called following a failed attempt. Client code does not deal with IRetryable directly, in other words, client code does not call Attempt and Recover. Instead, a Retryer class is used to encapsulate all generic retry logic.

The following figure illustrates the sequence diagram for the Retryer:

The client does not call Attempt and Recover methods directly, but rather through a Retryer which wraps the details of these two calls. The Retryer invokes ConcreteRetryable.Attempt operations and checks the return value. If the return value is true, the operation is successful and it returns control back to the client. If the return value is false, the Retryer calls ConcreteRetryable.Recover to perform any logic required to recover from the failed attempt. The Retryer may then wait for a configurable amount of time to allow the failed system to recover. After waiting, the Retryer invokes ConcreteRetryable.Attempt again. This cycle is then repeated until either an Attempt call is successful or it reaches the permitted maximum number of attempts. Usually, a small limit, say 5 retries, is sufficient.

Example

The following example implements Retryer for database query operations:

public interface IRetryable
{
    bool Attempt();
    void Recover();
}

public class RetryableQuery : IRetryable
{
    // Data members
   
private string m_strConnectionString = "";
    private string m_strSQL = "";
    private DataSet m_ds = new DataSet();

    // Constructor
   
public RetryableQuery( string strConn, string strSQL )
    {
        m_strConnectionString = strConn;
        m_strSQL = strSQL;
    }

    // IRetryable operation
   
public bool Attempt()
    {
        try
        {
            SqlConnection conn = new SqlConnection( m_strConnectionString );
            SqlCommand cmd = new SqlCommand();
            cmd.CommandText = m_strSQL;
            cmd.CommandType = CommandType.Text;

            SqlDataAdapter da = new SqlDataAdapter();
            da.SelectCommand = cmd;
            da.Fill( m_ds );
            return true;
        }
        catch ( SqlException ex )
        {
            // Return false for some pre-determined error numbers
           
switch (ex.Number)
            {
                case 1234:
                case 5678:
                case 1122:
                    return false;
                default:
                    throw new InvalidOperationException( "Operation has failed" );
            }
        }
    }

    public void Recover()
    {
        // Empty implementation. Nothing to do in this example
   
}

    // Public interface
    public DataSet GetResult()
    {
        return m_ds;
    }
}

public class Retryer
{
    private IRetryable  m_obRetryable;
    private int          m_nMaxRetryies = 5;    // Typically obtained from config files or database

    public Retryer(IRetryable retryable)
    {
        m_obRetryable = retryable;
    }

    // Retry the operation until is succeeds or retries reach the maximum allowed retry-limit 
   
public bool Invoke()
    {
        for ( int i =0; i < m_nMaxRetryies; i ++ )
        {
            if (m_obRetryable.Attempt())
                return true;
            else
                m_obRetryable.Recover();
        }
        return false;
    }
}

// Client code
void InvokeQuery()
{
    // Create retryable objects
    RetryableQuery ob = new RetryableQuery( "SomeConnection", "select * from T1" );
    Retryer obRetryer = new Retryer( ob );

    // Invoke a retryable method
    bool bRet = obRetryer.Invoke();

    // Get data if operation succeeded
    DataSet ds = null;
    if (bRet)
        ds = ob.GetResult();
}

Applicability

Use this pattern when:

Strategies / Variants

Consider these strategies when designing a retryer.

public class RetryableOpen : IRetryable
{
    private string m_strConnection;

    RetryableOpen( string str )
    {
        m_strConnection = str
    }

    // IRetryable
    public bool Attempt()
    {
        try
        {

        }
        catch( SomeExpectedException )
        {
             return false;
        }
    }

    public void Recover()
    {
        ...
    }

    // Properties
    public string ConnectionString
    {
        set {  m_strConnection = value; }
        get { return m_strConnection; }
    }
}

Benefits

Related Patterns