reactiveui / ReactiveUI

An advanced, composable, functional reactive model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming. ReactiveUI allows you to abstract mutable state away from your user interfaces, express the idea around a feature in one readable place and improve the testability of your application.

Home Page:https://www.reactiveui.net

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Bug]: ThrownExceptions doent get triggered when i run Parallel.ForEachAsync

M0n7y5 opened this issue ยท comments

Describe the bug ๐Ÿž

ThrownExceptions on my command doesnt get triggered when my async method raise an exception inside Parallel.ForEachAsync.

My command and exception handler registration

this.AnalyzeOriginalCommand = ReactiveCommand.CreateFromObservable(
    () =>
        Observable
            .FromAsync(async ct =>
            {
                try
                {
                    OriginalFiles.Clear();
                    ScanOriginalDirectory();
                    await AnalyzeOriginalFolder(ct);
                }
                catch (Exception)
                {
                    throw;
                }
            })
            .TakeUntil(this.CancelOperationCommand),
    origPathValid
);

this.AnalyzeOriginalCommand.ThrownExceptions.SubscribeOn(RxApp.MainThreadScheduler)
    .Subscribe(async ex =>
    {
        await MsgBoxError
            .Handle(
                new string[]
                {
                    Resources.msgErrorTitle,
                    string.Format(Resources.msgErrorFmt, ex.Message)
                }
            )
            .ToTask();
    });

AnalyzeOriginalFolder method contains logic that uses Parallel.ForEachAsync. For some reason when anything bad happen in that method, i will not get notified via ThrownExceptions. However it works when i comment out the method and just throw some exception for test.

Step to reproduce

Create command that executes some logic using Parallel.ForEachAsync
Subscribe to exception handler of that command
Throw exception inside the ForEachAsync

Reproduction repository

No response

Expected behavior

ThrownExceptions should get triggered when the exception happens.

Screenshots ๐Ÿ–ผ๏ธ

No response

IDE

Visual Studio 2022

Operating system

Windows 10

Version

22H2

Device

PC

ReactiveUI Version

19.5.41

Additional information โ„น๏ธ

No response

Forgot to mention that i tried it run without the try/catch rethrow logic.

Is there a specific reason for not using the latest release?
You may also want to try using Create.FromAsync with a cancellation token.
I need to update the documentation to make this the recommended way rather than the original Create.FromObservable recommendation due to an older bug, I believe I had fixed this prior to the 19.5.41 version you have used so should work as expected now.
I will check which version I introduced the fix in.
The issue your are experiencing is the same reason why Create.FromObservable was the original recommended way to create a cancellable command. Observable.FromAsync swallows exceptions in some scenarios and never returns a result.

@ChrisPulman Ok i just tested it with latest version 20.1.1 and still has the same issue. I tried to use FromAsync and StartAsync. Fails to catch exceptions in both cases. I just followed the docs. I do not know how exactly i am suppose to create cancellable command without FromObservable or somehow holding active subscription to ongoing execution since its called by the UI framework.

@ChrisPulman Ok i just tested it with latest version 20.1.1 and still has the same issue. I tried to use FromAsync and StartAsync. Fails to catch exceptions in both cases. I just followed the docs. I do not know how exactly i am suppose to create cancellable command without FromObservable or somehow holding active subscription to ongoing execution since its called by the UI framework.

I will create a little snippet this evening to show how to use CreateFromAsync with a Cancellation token basically you don't need to use the Observable.FromAsync in your code, there is an overload of CreateFromAsync that provides you with a Cancellation token

If you set a breakpoint on your throw; line, does it break in?

If you set a breakpoint on your throw; line, does it break in?

If you mean that debugger catch this yes. It catches all the exceptions no issues at all. It just does not bubble up to ThrownExceptions.

I can try to make repo for simple issue replication asap.

Depending on how the Commands Execute is being called the following should work.
Instead of using Observable.FromAsync use a ReactiveCommand with ReactiveCommand.CreateFromTask and use the cancellation token provided within your Async tasks

This example shows both cancellation and Exceptions being thrown within the executed code.

using ReactiveUI;
using System.Reactive;
using System.Reactive.Linq;

namespace ParallelForEachTest;

internal class Program
{
    private static void Main(string[] args)
    {
        Console.Out.WriteLine("Execute ForEach and cancel the operation");
        _ = new CancelationCommandTest(false);
        Console.ReadLine();

        Console.Out.WriteLine("Execute ForEach and cancel the operation with exception");
        _ = new CancelationCommandTest(true);
        Console.ReadLine();
    }
}

internal class CancelationCommandTest : ReactiveObject
{
    public CancelationCommandTest(bool throwException)
    {
        ThrownExceptions.Subscribe(ex => Console.Out.WriteLine("CancelationCommandTest class threw:" + ex.Message));

        CancelCommand = ReactiveCommand.Create(() => Console.Out.WriteLine("Cancel Requested!"));
        ForEachCommand = ReactiveCommand.CreateFromTask(async (ct) =>
        {
            ParallelOptions options = new()
            {
                CancellationToken = ct,
                MaxDegreeOfParallelism = Environment.ProcessorCount
            };
            await Parallel.ForEachAsync(Enumerable.Range(0, 1000), options, async (i, token) =>
            {
                if (token.IsCancellationRequested)
                {
                    Console.Out.WriteLine($"Operation canceled on {Environment.CurrentManagedThreadId}");
                    return;
                }

                await Task.Delay(1000, token);
                double d = Math.Sqrt(i);
                Console.Out.WriteLine($"The square root of {i} is {d} on {Environment.CurrentManagedThreadId}");
                if (throwException && i == 100)
                {
                    throw new Exception("Exception thrown on 100");
                }
            });
        });
        ExecuteForEachCommand = ReactiveCommand.CreateFromObservable(() => ForEachCommand.Execute().TakeUntil(CancelCommand));

        // Handle exceptions
        ExecuteForEachCommand.ThrownExceptions.Subscribe(ex => Console.Out.WriteLine("ExecuteForEachCommand threw:" + ex.Message));
        ForEachCommand.ThrownExceptions.Subscribe(ex => Console.Out.WriteLine("ForEachCommand threw:" + ex.Message));

        // Run test
        ExecuteForEachCommand.Execute().Subscribe();
        if (!throwException)
        {
            Task.Delay(5000).Wait();
            CancelCommand.Execute().Subscribe();
        }
    }

    public ReactiveCommand<Unit, Unit> CancelCommand { get; private set; }

    public ReactiveCommand<Unit, Unit> ForEachCommand { get; private set; }

    public ReactiveCommand<Unit, Unit> ExecuteForEachCommand { get; private set; }
}

If you are executing the command directly i.e. not using bindings, then you can achieve the same by creating a disposable and then disposing when you wish to cancel. This would remove the need for the ExecuteForEachCommand.

var disposable = ForEachCommand.Execute().TakeUntil(CancelCommand).Subscribe();

Ideally avoid using Observable.FromAsync with a cancellation token as Exceptions do not bubble as expected Replace any calls that use this with a ReactiveCommand and initialise it with the ReactiveCommand.CreateFromTask(async (ct) =>{}); method.

commented

@ChrisPulman Thx alot. Tho looking at the code, it feels too clunky to use. I'll stick with CreateFromTask and CancellationTokenSource for now as it makes code less cluttered. I will keep this issue opened until this bug, if it's a bug, is fixed.

Technically it's not a bug in ReactiveUI it's a bug with Observable.FromAsync with a cancellation token as Exceptions do not bubble as expected.

commented

So should i close this then?