I observed a strange phenomenon that occurs sometimes with an Rx query I wrote, that involves a CancellationToken
. Two callbacks are registered to the same CancellationToken
, one outside of the query and one that is part of the query. The intention of the CancellationToken
is to signal the termination of the query. What happens is that sometimes the second callback is stuck in the middle of the execution, never completing, preventing the first callback from being invoked.
Below is a minimal example that reproduces the issue. It's not very minimal, but I can't reduce it any further. For example replacing the Switch
operator with the Merge
makes the issue disappear. The same happens if the exception thrown by the Task.Delay(1000, cts.Token)
is swallowed.
public class Program
{
public static void Main()
{
var cts = new CancellationTokenSource(500);
cts.Token.Register(() => Console.WriteLine("### Token Canceled! ###"));
try
{
Observable
.Timer(TimeSpan.Zero, TimeSpan.FromMilliseconds(1000))
.TakeUntil(Observable.Create<Unit>(observer =>
cts.Token.Register(() =>
{
Console.WriteLine("Before observer.OnNext");
observer.OnNext(Unit.Default);
Console.WriteLine("After observer.OnNext");
})))
.Select(_ =>
{
return Observable.StartAsync(async () =>
{
Console.WriteLine("Action starting");
await Task.Delay(1000, cts.Token);
return 1;
});
})
.Switch()
.Wait();
}
catch (Exception ex) { Console.WriteLine("Failed: {0}", ex.Message); }
Thread.Sleep(500);
Console.WriteLine("Finished");
}
}
Expected output:
Action starting
Before observer.OnNext
After observer.OnNext
### Token Canceled! ###
Failed: A task was canceled.
Finished
Actual output (sometimes):
Action starting
Before observer.OnNext
Failed: A task was canceled.
Finished
Try it on fiddle. You may need to run the program 3-4 times before the issue appears. Notice the two missing log entries. It seems that the call observer.OnNext(Unit.Default);
never completes.
My question is: Does anyone have any idea what causes this issue? Also, how could I modify the CancellationToken
-related part of the query, so that it performs its intended purpose (terminates the query), without interfering with other registered callbacks of the same CancellationToken
?
.NET 5.0.1 & .NET Framework 4.8, System.Reactive 5.0.0, C# 9
Update: also .NET 6.0 with System.Reactive 5.0.0 (screenshot taken at June 4, 2022)
One more observation: The issue stops appearing if I modify the Observable.Create
delegate so that it returns a Disposable.Empty
instead of a CancellationTokenRegistration
, like this:
.TakeUntil(Observable.Create<Unit>(observer =>
{
cts.Token.Register(() =>
{
Console.WriteLine("Before observer.OnNext");
observer.OnNext(default);
Console.WriteLine("After observer.OnNext");
});
return Disposable.Empty;
}))
But I don't think that ignoring the registration returned by the cts.Token.Register
is a fix.