4
\$\begingroup\$

Previous version

Now supports async operations and cancellation.

Let’s say we copy some file using retry strategy (it might be blocked, etc.). App code comes bellow:

class Processor
{
    public void CopyData() =>
        CopyData(IOTry.Slow);

    public void CopyData(Try loop) =>
        loop.Execute(() => 
            File.Copy(@"c\a.txt", @"c:\b.txt"));
}

Where:

public class IOTry
{
    public static readonly Try Slow = Try.Retry(delay: 1000, times: 4, ratio: 3);
    public static readonly Try Fast = Try.Retry(delay: 100, times: 4, ratio: 3);
}

Library code is a way longer now - anybody see a way to simplify?

public abstract class Try
{
    public static readonly Try Never = new Never();
    public static readonly Try Once = Retry(delay: 0, times: 0, ratio: 0);

    public static Try Retry(int delay, int times, double ratio) =>
        RetryAfter(from i in Enumerable.Range(0, times)
                   select delay * Math.Pow(ratio, i) into d
                   select (int)d);

    public static Try RetryAfter(params int[] delays) => RetryAfter(delays.AsEnumerable());
    public static Try RetryAfter(IEnumerable<int> delays) => new Retry(delays);

    public void Execute(Action action) => Execute(action, CancellationToken.None);
    public abstract void Execute(Action action, CancellationToken cancellationToken);

    public Task ExecuteAsync(Action action) => ExecuteAsync(action, CancellationToken.None);
    public Task ExecuteAsync(Action action, CancellationToken cancellationToken) => 
        ExecuteAsync(() => { action(); return Task.CompletedTask; }, cancellationToken);

    public Task ExecuteAsync(Func<Task> action) => ExecuteAsync(action, CancellationToken.None);
    public abstract Task ExecuteAsync(Func<Task> action, CancellationToken cancellationToken);

    public Try FailFast() => FailFast(0);
    public Try FailFast(int timeout) => new Breaker(this, timeout);
}

And NULL Object pattern:

class Never : Try
{
    public override void Execute(Action action, CancellationToken cancellationToken) {}
    public override Task ExecuteAsync(Func<Task> action, CancellationToken cancellationToken) => Task.CompletedTask;
}

And:

class Retry : Try
{
    IEnumerable<int> Delays { get; }

    public Retry(IEnumerable<int> delays)
    {
        Delays = delays;            
    }

    public override void Execute(Action action, CancellationToken cancellationToken)
    {
        foreach (var delay in Delays)
            try
            {
                action();
                return;
            }
            catch
            {
                cancellationToken.WaitHandle.WaitOne(delay);
                cancellationToken.ThrowIfCancellationRequested();
            }

        action();
    }

    public override async Task ExecuteAsync(Func<Task> action, CancellationToken cancellationToken)
    {
        foreach (var delay in Delays)
            try
            {
                await action();
                return;
            }
            catch
            {
                await Task.Delay(delay, cancellationToken);
            }

        await action();
    }
}

And Circuit Breaker pattern:

class Breaker : Try
{
    Try Loop { get; }
    TimeSpan Timeout { get; }
    DateTime Ready { get; set; }

    public Breaker(Try loop, int timeout)
        : this(loop, TimeSpan.FromMilliseconds(timeout))
    {
    }

    public Breaker(Try loop, TimeSpan timeout)
    {
        Loop = loop;
        Timeout = timeout;
        Ready = DateTime.Now;
    }

    public override void Execute(Action action, CancellationToken cancellationToken)
    {
        if (Ready > DateTime.Now)
            throw new OperationCanceledException();

        try
        {
            Loop.Execute(action, cancellationToken);
        }
        catch
        {
            if (Timeout == TimeSpan.Zero)
                Ready = DateTime.MaxValue;
            else
                Ready = DateTime.Now + Timeout;

            throw;
        }
    }

    public override async Task ExecuteAsync(Func<Task> action, CancellationToken cancellationToken)
    {
        if (Ready > DateTime.Now)
            throw new OperationCanceledException();

        try
        {
            await Loop.ExecuteAsync(action, cancellationToken);
        }
        catch
        {
            if (Timeout == TimeSpan.Zero)
                Ready = DateTime.MaxValue;
            else
                Ready = DateTime.Now + Timeout;

            throw;
        }
    }
}
\$\endgroup\$
  • 5
    \$\begingroup\$ Your {}-less loops are a real challange for me :-P \$\endgroup\$ – t3chb0t Sep 1 '16 at 18:54
  • \$\begingroup\$ One could say that Python is an absolute winner in data science because of its readability – they do need it to deliver – it is not redundantly obfuscated with useless brackets :-P \$\endgroup\$ – Dmitry Nogin Sep 1 '16 at 19:09
  • 2
    \$\begingroup\$ I couldn't disagree more. I find python extremely hard to read for its lack of brackets. It's so easy to make a mistake by a simple indentation error. In VS you press a key combination and your code is perfectly formatted and you easily see the structure. In python you have to do it yourself and if you're lucky the code even works. \$\endgroup\$ – t3chb0t Sep 1 '16 at 19:26
  • \$\begingroup\$ You are definitely not alone on that side of the force :) The same time – just google “Python vs C#”; you will see almost everybody stating that Python is better in readability, C# has more consistent syntax link, etc. \$\endgroup\$ – Dmitry Nogin Sep 1 '16 at 19:35
  • \$\begingroup\$ How are clients of the API supposed to know that the constructor Breaker(Try loop, int timeout) expects milliseconds? I would name the parameter timeoutInMs (as clumsy as it sounds) or remove the constructor altogether. \$\endgroup\$ – Good Night Nerd Pride Sep 6 '16 at 11:43
2
\$\begingroup\$

I was just going to borrow your code and implement it in my application ;-) but there is one thing that I'm missing here. I needed to know which exceptions were thrown.

That's why I want to suggest you to extend the framework so that after the retries are finished you can log what happened.

I could imagine the new API like this:

class Processor
{
    private readonly ILogger _logger = ...;

    public void CopyData() =>
        CopyData(IOTry.Slow);

    public void CopyData(Try loop) =>
        loop.Execute(() => 
            File.Copy(@"c\a.txt", @"c:\b.txt"),
            ex => _logger.Log(ex)
        );
}

where the second lambda passes either each exception that occured or a collection of exceptions or an AggregateException - perhaps all three APIs make sense in different situations.

The API could either pass each exception that occured right away or collect them and pass them to me when the retry is finished. I'm not sure what could work best.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I would probably have void ILog.LogIfError(Action) and T ILog.LogIfError<T>(Func<T>) and put it inside of the existing lambda. \$\endgroup\$ – Dmitry Nogin Sep 6 '16 at 12:14
  • \$\begingroup\$ @DmitryNogin Mhmm, then it wouldn't make sense, to have a try/catch inside the action and another one in the retry, I thougt the retry could be used so that I don't have to try/catch everything.... \$\endgroup\$ – t3chb0t Sep 6 '16 at 12:16
  • \$\begingroup\$ I mean try.Execute(() => log.IfError(() => File.Copy("a","b"))); No more try catch actually to be written again and again. It could also be log.IfError(() => try.Execute(() => log.IfError(() => File.Copy("a","b")))); :) \$\endgroup\$ – Dmitry Nogin Sep 6 '16 at 12:25
  • \$\begingroup\$ @DmitryNogin oh no, a lambda-ception - undebuggable? However this would have one drawback, it would log only the last failure, but what if it works, then I wouldn't know about the first 30 attempts, would I want to ignore it?... with each question this becomes more and more interesting ;-] \$\endgroup\$ – t3chb0t Sep 6 '16 at 12:32
  • 1
    \$\begingroup\$ We could have a special decorator though, let me try to invent something :) \$\endgroup\$ – Dmitry Nogin Sep 6 '16 at 12:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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