-1

I've got two Transient Fault Handling/Retry pattern implementations.

The issue is that theTask.Run swallows the exception and it doesn't rethrow it out of the Task.Run scope.

If I await the Task.Run it would work, but I cannot do that in my real use case.

public static class PollyRetry
{
    public static T Do<T>(Func<T> action, TimeSpan retryWait, int retryCount = 0)
    {
        var policyResult = Policy
            .Handle<Exception>()
            .WaitAndRetry(retryCount, retryAttempt => retryWait)
            .ExecuteAndCapture(action);

        if (policyResult.Outcome == OutcomeType.Failure)
        {
            throw policyResult.FinalException;
        }

        return policyResult.Result;
    }

    public static async Task<T> DoAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 0)
    {
        var policyResult = await Policy
            .Handle<Exception>()
            .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
            .ExecuteAndCaptureAsync(action);

        if (policyResult.Outcome == OutcomeType.Failure)
        {
            throw policyResult.FinalException;
        }

        return policyResult.Result;
    }
}

public static class Retry
{
    public static void Do(Action action, TimeSpan retryInterval, int retryCount = 3)
    {
        Do<object?>(() =>
        {
            action();
            return null;
        }, retryInterval, retryCount);
    }

    public static async Task DoAsync(Func<Task> action, TimeSpan retryInterval, int retryCount = 3)
    {
        await DoAsync<object?>(async () =>
        {
            await action();
            return null;
        }, retryInterval, retryCount);
    }

    public static T Do<T>(Func<T> action, TimeSpan retryInterval, int retryCount = 3)
    {
        var exceptions = new List<Exception>();

        for (var count = 1; count <= retryCount; count++)
        {
            try
            {
                return action();
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
                if (count < retryCount)
                {
                    Thread.Sleep(retryInterval);
                }
            }
        }

        throw new AggregateException(exceptions);
    }

    public static async Task<T> DoAsync<T>(Func<Task<T>> func, TimeSpan retryInterval, int retryCount = 3)
    {
        var exceptions = new List<Exception>();

        for (var count = 1; count <= retryCount; count++)
        {
            try
            {
                return await func();
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
                if (count < retryCount)
                {
                    await Task.Delay(retryInterval);
                }
            }
        }

        throw new AggregateException(exceptions);
    }
}

public sealed class WebSocketClient
{
    private readonly Channel<string> _receiveChannel;
    private readonly Channel<string> _sendChannel;

    public WebSocketClient()
    {
        _receiveChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10)
        {
            SingleWriter = true,
            SingleReader = false,
            FullMode = BoundedChannelFullMode.DropOldest
        });

        _sendChannel = Channel.CreateBounded<string>(new BoundedChannelOptions(10)
        {
            SingleReader = true,
            SingleWriter = false,
            FullMode = BoundedChannelFullMode.Wait
        });
    }

    public async Task StartWithRetry(Uri uri)
    {
        await Retry.DoAsync(() => Task.FromResult(StartAsync(uri)), TimeSpan.FromSeconds(5), 5);
    }

    public async Task StartAsync(Uri uri)
    {
        using var ws = new ClientWebSocket();
        await ws.ConnectAsync(uri, default);

        if (ws.State == WebSocketState.Open)
        {
            const string message = "{\"op\": \"subscribe\", \"args\": [\"orderBookL2_25:XBTUSD\"]}";
            var buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(message));
            await ws.SendAsync(buffer, WebSocketMessageType.Text, true, default);
        }

        _ = Task.Run(async () =>
        {
            while (await _receiveChannel.Reader.WaitToReadAsync())
            {
                while (_receiveChannel.Reader.TryRead(out var message))
                {
                    Console.WriteLine($"Message: {message}");
                }
            }
        });

        _ = Task.Run(async () =>
        {
            // This throws WebSocketException with ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely
            while (true)
            {
                ValueWebSocketReceiveResult receiveResult;

                using var buffer = MemoryPool<byte>.Shared.Rent(4096);
                await using var ms = new MemoryStream(buffer.Memory.Length);
                do
                {
                    receiveResult = await ws.ReceiveAsync(buffer.Memory, default);

                    if (receiveResult.MessageType == WebSocketMessageType.Close)
                    {
                        break;
                    }

                    await ms.WriteAsync(buffer.Memory[..receiveResult.Count]);
                } while (!receiveResult.EndOfMessage);

                ms.Seek(0, SeekOrigin.Begin);

                if (receiveResult.MessageType == WebSocketMessageType.Text)
                {
                    using var reader = new StreamReader(ms, Encoding.UTF8);
                    var message = await reader.ReadToEndAsync();

                    await _receiveChannel.Writer.WriteAsync(message);
                }
                else if (receiveResult.MessageType == WebSocketMessageType.Close)
                {
                    break;
                }
            }
        });
    }
}

Minimal Reproducible Example

var code = new MinimalReproducibleCode();
await code.StartWithRetry();

public sealed class MinimalReproducibleCode
{
    public async Task StartWithRetry()
    {
        await Retry.DoAsync(() => Task.FromResult(StartAsync()), TimeSpan.FromSeconds(5), 5);
    }
    
    public Task StartAsync()
    {
        Console.WriteLine("This has just started");
        
        _ = Task.Run(() =>
        {
            while (true)
            {
                Console.WriteLine("Code is working");

                throw new DivideByZeroException();
            }
        });
        
        return Task.CompletedTask;
    }
}

public static class Retry
{
    public static void Do(Action action, TimeSpan retryInterval, int retryCount = 3)
    {
        _ = Do<object?>(() =>
        {
            action();
            return null;
        }, retryInterval, retryCount);
    }

    public static async Task DoAsync(Func<Task> action, TimeSpan retryInterval, int retryCount = 3)
    {
        _ = await DoAsync<object?>(async () =>
        {
            await action();
            return null;
        }, retryInterval, retryCount);
    }

    public static async Task DoAsync<TException>(
        Func<Task> action,
        Func<TException, bool> exceptionFilter,
        TimeSpan retryInterval,
        int retryCount = 3) where TException : Exception
    {
        _ = await DoAsync<object?>(async () =>
        {
            await action();
            return null;
        }, retryInterval, retryCount);
    }

    public static T Do<T>(Func<T> action, TimeSpan retryWait, int retryCount = 3)
    {
        var policyResult = Policy
            .Handle<Exception>()
            .WaitAndRetry(retryCount, retryAttempt => retryWait)
            .ExecuteAndCapture(action);

        if (policyResult.Outcome == OutcomeType.Failure)
        {
            throw policyResult.FinalException;
        }

        return policyResult.Result;
    }

    public static async Task<T> DoAsync<T>(Func<Task<T>> action, TimeSpan retryWait, int retryCount = 3)
    {
        var policyResult = await Policy
            .Handle<Exception>()
            .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
            .ExecuteAndCaptureAsync(action);

        if (policyResult.Outcome == OutcomeType.Failure)
        {
            throw policyResult.FinalException;
        }

        return policyResult.Result;
    }

    public static async Task<T> DoAsync<T, TException>(
        Func<Task<T>> action,
        Func<TException, bool> exceptionFilter,
        TimeSpan retryWait,
        int retryCount = 0) where TException : Exception
    {
        var policyResult = await Policy
            .Handle(exceptionFilter)
            .WaitAndRetryAsync(retryCount, retryAttempt => retryWait)
            .ExecuteAndCaptureAsync(action);

        if (policyResult.Outcome == OutcomeType.Failure)
        {
            throw policyResult.FinalException;
        }

        return policyResult.Result;
    }
}

nop
  • 4,711
  • 6
  • 32
  • 93
  • 1
    What are you asking us? You have just made statements. – Enigmativity Oct 14 '22 at 00:07
  • 2
    Also, that's a boat load of code. Can you please provide a [mcve]? – Enigmativity Oct 14 '22 at 00:09
  • @Enigmativity, hi mate, what I'm asking is how do I make it actually retry because right now it doesn't since the Task.Runs are just swallowing these exceptions. – nop Oct 14 '22 at 05:09
  • @Enigmativity, just added a minimal reproducible example – nop Oct 14 '22 at 05:31
  • 1
    @nop The exceptions aren't being swallowed - your code just isn't providing anywhere for them to be exposed. In particular, your `StartAsync` method is incorrect for that reason (e.g. you're discarding the `Task` returned from `Task.Run`, which is the `Task` that will expose the exception: **so don't discard it**). – Dai Oct 14 '22 at 05:46
  • 2
    `() => Task.FromResult(StartAsync())` <-- Why are you doing this? – Dai Oct 14 '22 at 05:47
  • @Dai, `await Retry.DoAsync(() => StartAsync(uri), TimeSpan.FromSeconds(5), 5)` corrected it – nop Oct 14 '22 at 05:51
  • @nop - It's not a [mcve] for me yet. I've added Polly via NuGet but the Retry class doesn't have a `DoAsync` method. Can you help me get this running? – Enigmativity Oct 15 '22 at 05:46
  • @Enigmativity, it was right above, but I added it to the minimal reproducible example too. – nop Oct 15 '22 at 07:54

1 Answers1

1

OK, based on your code, here's how to make it work:

public async Task StartWithRetry()
{
    await Retry.DoAsync(() => StartAsync(), TimeSpan.FromSeconds(5), 5);
}

public async Task StartAsync()
{
    Console.WriteLine("This has just started");

    await Task.Run(() =>
    {
        while (true)
        {
            Console.WriteLine("Code is working");

            throw new DivideByZeroException();
        }
    });
}

You need to await the Task.Run and not fire-and-forget it.

When I run the above code I get:

This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
This has just started
Code is working
DivideByZeroException
Attempted to divide by zero.
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Thanks mate! However, it's not applicable in my real scenario. I guess `await await Task.WhenAll(task1, task2);` should work. – nop Oct 15 '22 at 23:07