-
Notifications
You must be signed in to change notification settings - Fork 751
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Observable.FromAsync produces unobserved task exceptions #1256
Comments
Has been a bit of a headache as well. Solution I found in my specific case was to use Unsure if it always works, though... |
This issue cant be solved by using the Materialize() option for all cases, the Observable just completes and does not handle Async Cancellation correctly, it should always produce a result. |
@clairernovotny it would really help if you can add fixing this to a higher priority list and make a public release with the fix. Many thanks in advance. |
[Fact]
public async Task ObservableFromAsyncHandlesCancellation()
{
var statusTrail = new List<(int, string)>();
var position = 0;
Exception exception = null;
var fixture = Observable.FromAsync(async (token) =>
{
var ex = new Exception();
statusTrail.Add((position++, "started command"));
try
{
await Task.Delay(7000, token).ConfigureAwait(true);
}
catch (OperationCanceledException oce)
{
statusTrail.Add((position++, "starting cancelling command"));
// dummy cleanup
await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false);
statusTrail.Add((position++, "finished cancelling command"));
throw new Exception("break execution", oce);
}
statusTrail.Add((position++, "finished command Normally"));
throw new Exception("break execution");
}).Catch<Unit, Exception>(
ex =>
{
//// should be OperationCanceledException
//// OR user code exception
exception = ex;
statusTrail.Add((position++, "Exception Should Be here"));
return Observable.Throw<Unit>(ex);
}).Finally(() => statusTrail.Add((position++, "Should ONLY come here Last")));
var cancel = fixture.Subscribe();
if (true)
{
//// This operation is not as expected
await Task.Delay(500).ConfigureAwait(true);
cancel.Dispose();
// Wait 5050 ms to allow execution and cleanup to complete
await Task.Delay(5050).ConfigureAwait(false);
Assert.True(statusTrail.Select(x => x.Item2).Contains("finished cancelling command"));
Assert.Equal("Should ONLY come here Last", statusTrail.Last().Item2);
//// (0, "started command")
//// (1, "Should ONLY come here Last")
//// (2, "starting cancelling command")
//// (3, "finished cancelling command")
}
else
{
//// This operates as expected
// Wait 7010 ms to allow execution and cleanup to complete
await Task.Delay(7010).ConfigureAwait(false);
Assert.True(statusTrail.Select(x => x.Item2).Contains("Exception Should Be here"));
Assert.Equal("Should ONLY come here Last", statusTrail.Last().Item2);
//// (0, "started command")
//// (1, "finished command Normally")
//// (2, "Exception Should Be here")
//// (3, "Should ONLY come here Last")
}
} |
[Fact]
public async Task ObservableFromAsyncHandlesCancellationWithResult()
{
var statusTrail = new List<(int, string)>();
var position = 0;
Exception exception = null;
var fixture = Observable.FromAsync(async (token) =>
{
statusTrail.Add((position++, "started command"));
try
{
await Task.Delay(7000, token).ConfigureAwait(true);
}
catch (OperationCanceledException oce)
{
statusTrail.Add((position++, "starting cancelling command"));
// dummy cleanup
await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false);
statusTrail.Add((position++, "finished cancelling command"));
return new Exception("break execution", oce);
}
statusTrail.Add((position++, "finished command Normally"));
return new Exception("break execution");
}).Catch<Exception, Exception>(
ex =>
{
//// should be OperationCanceledException
//// OR user code exception
exception = ex;
statusTrail.Add((position++, "Exception Should Be here"));
return Observable.Throw<Exception>(ex);
}).Finally(() => statusTrail.Add((position++, "Should ONLY come here Last")));
var cancel = fixture.Subscribe(x => exception = x);
if (true)
{
//// This operation is not as expected
await Task.Delay(500).ConfigureAwait(true);
cancel.Dispose();
// Wait 5050 ms to allow execution and cleanup to complete
await Task.Delay(5050).ConfigureAwait(false);
Assert.True(statusTrail.Select(x => x.Item2).Contains("finished cancelling command"));
Assert.True(statusTrail.Select(x => x.Item2).Contains("Exception Should Be here"));
Assert.Equal("Should ONLY come here Last", statusTrail.Last().Item2);
//// (0, "started command")
//// (1, "Should ONLY come here Last")
//// (2, "starting cancelling command")
//// (3, "finished cancelling command")
}
else
{
//// This operates as expected
// Wait 7010 ms to allow execution and cleanup to complete
await Task.Delay(7010).ConfigureAwait(false);
Assert.Equal("Should ONLY come here Last", statusTrail.Last().Item2);
Assert.Equal("break execution", exception.Message);
//// (0, "started command")
//// (1, "finished command Normally")
//// (2, "Exception Should Be here")
//// (3, "Should ONLY come here Last")
}
} Hopefully these will help you find the issue |
Here's a different take on the issue: The exception must be left unobserved because that's what it is. The subscriber cancelled its subscription before it could observe it. The user's code didn't setup the try-catch handlers to handle the exception. Rx code could observe it but doesn't know how to handle it - observing it just for the sake of observing it (in a catch-em-all approach) is bad style IMO. Thus, it's rightfully considered unobserved and should be treated as such. I disagree that it is Rx duty to handle the exception. Yes, as @lezzi pointed out, it may be responsible for task creation but it's the user's code that throws. About being completely UnobservedTaskException-event-free: I see that |
Note that the following sequence is totally expected: //// (0, "started command")
//// (1, "Should ONLY come here Last")
//// (2, "starting cancelling command")
//// (3, "finished cancelling command") When
That is, the There are two things left to discuss. First,
This is part of the async method's behavior to have some awareness of cancellation and tie it up to the There are other cases where this translation doesn't happen though, e.g.
or
But it's unclear to me whether these should be treated differently from any other exception for a I'm with @danielcweber here from a purity point of view around dropping exceptions, though I can see there's an arguable inconsistency with e.g. an exception thrown from a |
@bartdesmet Thanks for your detailed explanation of how it is operating at present, and understandably some may rely on the Finally being hit without any exceptions on the Catch function when a However it does not fit to every scenario. In almost every case we want to know when an exception was produced during execution, as the Cancellation causes the execution to stop without completing it should be reported as an error. I trust that you can see our point of view and will be able to provide a suitable function to cater for our needs. |
I'm getting a massive amount of crash reports in Raygun (production) due to this, which was not happening in the past but I don't know what version introduced the issue. |
Hello, is it possible to resurrect this? @clairernovotny @bartdesmet @ChrisPulman , since this is hurting ReactiveCommand a lot. I have dealt with like 6 people struggling with this issue within short timespan |
It looks to me like there are two different proposals on the table:
I believe 1 is what @lezzi asked for in arguing for consistency with other scenarios, such as when the selector callback for a
I don't think the argument that such exceptions should be ignored is entirely watertight, even for cases like I think this is effectively what @bartdesmet proposed in his suggestion for an optional But I think @ChrisPulman is asking for something slightly different:
This actually rolls two behaviours into one statement (presumably to underscore the apparent consistency of this proposal). But we could (and in my view should) separate it back out into:
The scenario described in 1 is one where we don't unsubscribe, and in that case, the behaviour is already as described. The absence of any unsubscription in that case makes it very different from 2, which is why I think it's better to consider these separately. So it's really just 2—what happens when unsubscription occurs at a critical moment—that's under discussion here. So I think what we have is:
Also, I think that in this thread there has been a tendency to conflate unsubscription (calling
This conflation disguises important aspects of the problem because there are two separate things—the call to
(The "error" and "finally" here are copied from @ChrisPulman's message. I am interpreting these as referring to the calls to the callbacks passed to By showing the call to and, critically, the return from An alternative would be to allow execute Task => but this creates a new problem: in this model, notifications continue to be passed to the observer even after the call to So here's where I'm at with my understanding of the two quite different solutions being asked for here:
It's not clear to me which, if either, of these two proposals would solve the problems that ReactiveCommand is having. |
Context
Recently I discovered an uncertain behavior in
Observable.FromAsync
method which causes unobserved exceptions to be thrown.When there is an active subscription and task throws an exception everything works as expected - exception is catched by RX and forwarded to
onError
handler. The problem happens when subscription is cancelled right before the task failure. Under the hood (insideSlowTaskObservable
) RX cancels task continuation,Exception
property is not accessed by anyone and when GC is triggered - task unobserved exception is thrown.My main concerns here are next:
Select
(or anywhere else) and if subscription is cancelled at this point - exception is not forwarded anywhere and is "silently" ignored.Exception
property. But when subscription is cancelled behavior changes completely.TaskScheduler.UnobservedTaskException
. AnyObservable.FromAsync
usage can cause an unobserved exception. This part is critical in my view.How I expect it to work: if RX creates a task and takes responsibility for error handling, it should do it fully from the beginning to the end. Behavior has to be consistent, no subscription means we are no longer interested in errors and we shouldn't get them anywhere.
The text was updated successfully, but these errors were encountered: