-
-
Notifications
You must be signed in to change notification settings - Fork 929
Description
First of all, I would like to thank you for the amazing plugin that we actively use in our games.
Some time ago, we encountered unexpected behavior:
We had code that was supposed to call Dispose() when going out of scope. Then, there was a call to FirstAsync(), which was waiting for the required state, and after that, Dispose() was supposed to be called. However, in some cases, Dispose() was never called.
private readonly AsyncReactiveProperty<State> _state = new(State.None);
private async UniTask MyMethodAsync(CancellationToken cancellationToken)
{
using var myObj = new MyObj();
await _state.FirstAsync(x => x == State.MyState, cancellationToken);
}
This happened because a canceled token was passed to FirstAsync() (the developer did not check the token before calling the method). That is, in a situation when the developer did not check for the token to be canceled there will be a situation when the await method will await always.
Another example:
CancellationTokenSource cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
var rp = new AsyncReactiveProperty<int>(9);
rp.ForEachAsync(x =>
{
Debug.Log(x);
}, cancellationToken).Forget();
rp.Value = 10; // push 10 to all subscriber
rp.Value = 11; // push 11 to all subscriber
cts.Cancel();
rp.Value = 12; // push 12 to all subscriber
// Output
9
10
11
Right, after 11 we canceled the token and 12 will no longer be displayed.
And if we change the example:
CancellationTokenSource cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
cts.Cancel();
var rp = new AsyncReactiveProperty<int>(9);
rp.ForEachAsync(x =>
{
Debug.Log(x);
}, cancellationToken).Forget();
rp.Value = 10; // push 10 to all subscriber
rp.Value = 11; // push 11 to all subscriber
rp.Value = 12; // push 12 to all subscriber
// Output
9
But I, as a developer, expect the code to throw an OperationCanceledException.
According to Microsoft recommended patterns for CancellationToken ( https://devblogs.microsoft.com/premier-developer/recommended-patterns-for-cancellationtoken/ ) I suggest checking the token at the method input and throwing an exception (to detect errors in the calling code).
I made edits in my branch for LINQ methods (add checking cancellation), and added a test that passes with the edits, it does not pass on the current version of UniTask, but pass in my branch.
[Fact]
public async Task FirstAsync_WithCanceledCancellationToken_ThrowsOperationCanceledException()
{
// Arrange
var rp = new AsyncReactiveProperty<int>(99);
var cts = new CancellationTokenSource();
cts.Cancel();
var cancellationToken = cts.Token;
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
{
await rp.FirstAsync(x => x == 99, cancellationToken);
});
}
What do you think ?