3
\$\begingroup\$

The delay sequence has been fixed so I can move to the next step which are the Retry and Breaker.

(Just ignore the console output here and there. There is no logging yet so it helps me to see what's going on in LINQPad).

Both are derived from a common type Try:

public abstract class Try
{
    public abstract bool Execute(Action action, Action<AttemptInfo> onException)

    public virtual bool Execute(Action action)
    {
        return Execute(action, ex => { });
    }

    public abstract Task<bool> ExecuteAsync(Action action, CancellationToken cancellationToken, Action<AttemptInfo> onException);

    public Task<bool> ExecuteAsync(Action action)
    {
        return ExecuteAsync(action, CancellationToken.None, ex => { });
    }
}

The Retry provides some additional fields about the state and provides feedback to the calling method via the AttemptInfo type.

public class AttemptInfo
{
    internal AttemptInfo(Exception exception, int count)
    {
        Exception = exception;
        Count = count;
    }

    public Exception Exception { get; }

    public int Count { get; }

    public bool Handled { get; set; }
}

public class Retry : Try
{
    private readonly DelaySequence _delaySequence;

    private readonly List<Exception> _exceptions = new List<Exception>();

    public IEnumerable<Exception> Exceptions => _exceptions.AsReadOnly();

    public int Count { get; private set; }

    public Retry(DelaySequence delaySequence)
    {
        _delaySequence = delaySequence;
    }

    public override bool Execute(Action action, Action<AttemptInfo> onException)
    {
        foreach (var delay in _delaySequence)
        {
            try
            {
                Count++;
                action();
                return true;
            }
            catch (Exception ex)
            {
                _exceptions.Add(ex);
                var attempt = new AttemptInfo(ex, Count);
                onException(attempt);
                if (!attempt.Handled) { throw; }
            }
            Thread.Sleep(delay);
        }
        return false;
    }

    public override async Task<bool> ExecuteAsync(Action action, CancellationToken cancellationToken, Action<AttemptInfo> onException)
    {
        foreach (var delay in _delaySequence)
        {
            cancellationToken.ThrowIfCancellationRequested();
            try
            {
                Count++;
                action();
                return true;
            }
            catch (Exception ex)
            {
                _exceptions.Add(ex);
                var attempt = new AttemptInfo(ex, Count);
                onException(attempt);
                if (!attempt.Handled) { throw; }
            }
            await Task.Delay(delay, cancellationToken);
        }
        return false;
    }
}

The Breaker acts as a decorator for the Retry.

public class Breaker : Try
{
    private readonly Retry _retry;

    public Breaker(Retry retry, BreakerThreshold threshold)
    {
        _retry = retry;
        Threshold = threshold;
    }

    public BreakerThreshold Threshold { get; }

    public DateTime? LastExceptionOn { get; private set; }

    public int ExceptionCount { get; private set; }

    public BreakerState State { get; private set; }

    private void Reset()
    {
        LastExceptionOn = null;
        ExceptionCount = 0;
        State = BreakerState.Closed;
    }

    public override bool Execute(Action action, Action<AttemptInfo> onException)
    {
        return _retry.Execute(() =>
        {
            var hasOpenTimedout = (DateTime.UtcNow - LastExceptionOn) > Threshold.Timeout;
            if (Threshold.HasTimedout(LastExceptionOn))
            {
                Reset();
                Console.WriteLine("You may try again.");
            }

            if (State == BreakerState.Closed)
            {
                action();
            }
        }, attempt =>
        {
            if (Threshold.Exceeded(LastExceptionOn, ExceptionCount))
            {
                State = BreakerState.Open;
                attempt.Handled = true;
                Console.WriteLine("It's enough for now!" + Thread.CurrentThread.ManagedThreadId);
                return;
            }

            LastExceptionOn = DateTime.UtcNow;
            ExceptionCount++;

            onException(attempt);
        });
    }

    public override async Task<bool> ExecuteAsync(Action action, CancellationToken cancellationToken, Action<AttemptInfo> onException)
    {
        return await _retry.ExecuteAsync(() =>
        {
            var hasOpenTimedout = (DateTime.UtcNow - LastExceptionOn) > Threshold.Timeout;
            if (hasOpenTimedout)
            {
                Reset();
                Console.WriteLine("You may try to dig again.");
            }

            if (State == BreakerState.Closed)
            {
                action();
            }
        }, cancellationToken, attempt =>
        {
            if (Threshold.Exceeded(LastExceptionOn, ExceptionCount))
            {
                State = BreakerState.Open;
                attempt.Handled = true;
                Console.WriteLine("It's enough!" + Thread.CurrentThread.ManagedThreadId);
                return;
            }

            LastExceptionOn = DateTime.UtcNow;
            ExceptionCount++;

            onException(attempt);
        });
    }
}

It's supported by two helper types:

public enum BreakerState
{
    Closed,
    Open,
    //HalfOpen, // I didn't find any use for this yet.
}

public class BreakerThreshold
{
    public BreakerThreshold(int maxExceptionCount, TimeSpan timespan, TimeSpan timeout)
    {
        MaxExceptionCount = maxExceptionCount;
        TimeSpan = TimeSpan;
        Timeout = timeout;
    }

    public int MaxExceptionCount { get; }

    public TimeSpan TimeSpan { get; }

    public TimeSpan Timeout { get; }

    public bool Exceeded(DateTime? exceptionOn, int count)
    {
        return
            exceptionOn.HasValue &&
            DateTime.UtcNow - exceptionOn > TimeSpan &&
            count > MaxExceptionCount;
    }

    public bool HasTimedout(DateTime? exceptionOn)
    {
        return (DateTime.UtcNow - exceptionOn) > Timeout;
    }   
}

Usage example:

var counter = 0;

var failingAction = new Action(() =>
{
    if (counter++ < 6)
    {
        Console.WriteLine($"Plant potato! Attempt {counter}");
        throw new Exception("Soil too hard to dig!");
    }
    Console.WriteLine($"Finally planted! Attempt: {counter}");
});


var retry = new Retry(new RegularDelaySequence(TimeSpan.FromSeconds(0.5), 30));
var breaker = new Breaker(retry, new BreakerThreshold(2, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3)));

breaker.Execute(failingAction, attempt =>
{
    attempt.Handled = true;
});
// Wait for the breaker to timeout.
Thread.Sleep(3200);
breaker.Execute(failingAction, attempt =>
{
    attempt.Handled = true;
});

Output:

Plant potato! Attempt 1
Soil too hard to dig!
Plant potato! Attempt 2
Soil too hard to dig!
Plant potato! Attempt 3
Soil too hard to dig!
Plant potato! Attempt 4
It's enough for now!
You may try again.
Plant potato! Attempt 5
Soil too hard to dig!
Plant potato! Attempt 6
Soil too hard to dig!
Finally planted! Attempt: 7

What I particularly don't like are the 99% identical loops in the sync and async APIs. They differ only by the wait and by the async keyword.

Oh, and one more thing. I know there is something like Polly but I like to have things my way besides it helped me to understand a few things already so it's a good exercise at the same time.

\$\endgroup\$
7
  • \$\begingroup\$ Are you doing this just to learn or why not use The Transient Fault Handling Application Block? \$\endgroup\$ Nov 10, 2016 at 19:38
  • \$\begingroup\$ @CharlesNRice because the Enterprise Library has not been developed for many years and it crashes all over the place. Just thinking of the logging block, never again ;-] \$\endgroup\$
    – t3chb0t
    Nov 10, 2016 at 19:40
  • \$\begingroup\$ @t3chb0t I've only used Unity and the Transient Fault Handling Block and have never had issues with either. \$\endgroup\$ Nov 10, 2016 at 19:43
  • 1
    \$\begingroup\$ @t3chb0t Re: methods being 99% identical, can't you just extract the try/catch bit, which seems identical, into its own method, and call that method from sync/async Execute methods? Like that you also separate the "execute" logic from the "what to do before/after execute" logic. \$\endgroup\$
    – 404
    Nov 11, 2016 at 10:54
  • \$\begingroup\$ @eurotrash this acutally makes sense. I don't know why I was thinking about the entire loops the whole time. \$\endgroup\$
    – t3chb0t
    Nov 11, 2016 at 10:56

1 Answer 1

1
\$\begingroup\$

You can get rid of some code duplication by extracting the try/catch block in the Execute methods to a new method:

private bool ExecuteAction(Action action, Action<AttemptInfo> onException)
{
    try
    {
        Count++;
        action();
        return true;
    }
    catch (Exception ex)
    {
        _exceptions.Add(ex);
        var attempt = new AttemptInfo(ex, Count);
        onException(attempt);
        if (!attempt.Handled) { throw; }
        return false;
    }
}

Then both Execute methods can call this method instead:

public override bool Execute(Action action, Action<AttemptInfo> onException)
{
    foreach (var delay in _delaySequence)
    {
        if (ExecuteAction(action, onException))
            return true;

        Thread.Sleep(delay);
    }
    return false;
}

public override async Task<bool> ExecuteAsync(Action action, CancellationToken cancellationToken, Action<AttemptInfo> onException)
{
    foreach (var delay in _delaySequence)
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (ExecuteAction(action, onException))
            return true;

        await Task.Delay(delay, cancellationToken);
    }
    return false;
}

(I wish I could just call it in a single line and it either returns a value or throws an exception, but you only return the result if it's true.)

Now the rest of the duplication becomes more obvious - I'd like to extract the loop and final return false to a new method that takes two more delegates - one as a pre-execution action (cancellationToken.ThrowIfCancellationRequested() for the async method, nothing for the sync method) and one for the post-execution action (Thread.Sleep(delay), await Task.Delay(delay, cancellationToken)). Each public method would then call that private method with four delegate parameters, and the entirety of the loop/execution could be in one method. But as my C# knowledge is really based on 4.0, I don't know enough about async/await to know whether that's feasible.

\$\endgroup\$
1
  • \$\begingroup\$ I must say it would be perfect if I could only have one method that does the job for sync and async but I'm not that good with this stuff so I need to keep them both. But what I can implement is generic version of the ExecuteX that returns something. I return a bool because I didn't want to have another try/catch around this. I wanted the Retry to handle everything for me. \$\endgroup\$
    – t3chb0t
    Nov 11, 2016 at 17:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.