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 1Soil too hard to dig!Plant potato! Attempt 2Soil too hard to dig!Plant potato! Attempt 3Soil too hard to dig!Plant potato! Attempt 4It's enough for now!You may try again.Plant potato! Attempt 5Soil too hard to dig!Plant potato! Attempt 6Soil too hard to dig!Finally planted! Attempt: 7What 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.
Try again, and again, and again... but not too often because the potatoes won't grow
c#;design patterns;error handling;inheritance;async await
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.