-
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
Issue with CancellationToken lifetime in Observable.Using (async version) #2089
Comments
The cancellation token's purpose is not to tell you that the subscription has ended (although under certain circumstances, unsubscription will trigger the thing that it's actually telling you about). Its purpose is to tell the callback that the result the callback was meant to produce is no longer required. This is related to the subscription only insofar as if someone unsubscribes before your callback returns, cancellation may occur. But there are no guarantees at all about what that token does after your callback returns. Arguably we have a documentation bug here, because where the docs say that these tokens allow:
it is at best unclear, and possibly misleading, to say "usage" here. You could reasonably interpret the resource as being "used" for the entire lifetime of the subscription. I think this should really have said:
So we use the resource to create an observable. (That's the cancellable part of "using" the resource.) And then we go on to use the resulting The design intent is that the cancellation token's state is only meaningful for as long as the callback is running. Once the callback has returned, there's no longer any work left to cancel. Why am I saying that's the design intent?
So in your example, I wouldn't have expected the cancellation tokens to be signalled at all. However, as the documentation says, this method supports:
So that means two things:
Since cancellation is described as 'best effort' here, the guarantees are very weak. The only thing that is guaranteed is that each cancellation token won't be signalled while the callback is running if the result of the callback is still required. There's definitely something a bit peculiar about the behaviour this example produces. I would not have expected the cancellation token to get signalled when nothing needed to be cancelled. (And both of your callbacks run to completion, and the subscription continue to run for some time after that. It's definitely too late to cancel a callback if that callback already returned!) But since the behaviour of the token after it has served its useful purpose is, strictly speaking, undefined, technically it is not a bug the token may be signalled long after it's useful, and that's what you're actually seeing here. I just want to clarify why I'm saying it's occurring after it's useful (i.e., later than we might reasonably expect), when you're reporting that it's coming earlier than you think it should. The behaviour you describe as expected behaviour:
is also not what I would have expected here. Here's the output I was expecting:
I.e., I wasn't expecting either of the cancellation tokens to get cancelled, because both of the callbacks ran to completion (meaning that there was no work left to cancel). If you interpret the documentation ("any stage of the resource acquisition or usage") to extend to the full lifetime of the subscription, your interpretation makes sense. But this has never been what the implementation was actually trying to achieve. So that's why I'm classifying this as a documentation bug. If you want to know when the resource is no longer in use, the call to I think it would be cleaner if they never got cancelled in this situation. But there's every chance that code out there somewhere is relying on the fact that they do get cancelled after the work they were meant to govern completes, so I think we're kind of stuck with that behaviour. So in conclusion:
|
Bug
In the async version of Observable.Using, in observable factory delegate cancellationToken gets canceled not at the moment of destroying subscription. The synchronous version looks ok as there are no cancellation tokens.
Reproduced in System.Reactive.Linq 6.0.0, x64, Win11, dotnet version 8.0.100
Actual output:
Expected output:
The text was updated successfully, but these errors were encountered: