dotnet / reactive

The Reactive Extensions for .NET
http://reactivex.io
MIT License
6.73k stars 751 forks source link

Observable.Delay throws PlatformNotSupportedException on Blazor WASM. #2061

Open jeremy-morren opened 12 months ago

jeremy-morren commented 12 months ago

Bug

On Blazor WASM, Observable.Delay is not implemented.

Index.razor

@page "/"
@using System.Reactive.Linq
@using System.Reactive.Threading.Tasks

@code {

    protected override async Task OnInitializedAsync()
    {
        await Observable.Return(1)
            .Delay(TimeSpan.FromSeconds(1))
            .ToTask();
    }

}

Exception:

System.PlatformNotSupportedException: Operation is not supported on this platform.
   at System.Threading.Thread.ThrowIfNoThreadStart(Boolean internalThread)
   at System.Threading.Thread.Start(Object parameter, Boolean captureContext, Boolean internalThread)
   at System.Threading.Thread.Start(Object parameter)
   at System.Reactive.Concurrency.ConcurrencyAbstractionLayerImpl.StartThread(Action`1 action, Object state)
   at System.Reactive.Concurrency.DefaultScheduler.LongRunning.LongScheduledWorkItem`1[[System.Threading.CancellationToken, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]..ctor(CancellationToken state, Action`2 action)
   at System.Reactive.Concurrency.DefaultScheduler.LongRunning.ScheduleLongRunning[CancellationToken](CancellationToken state, Action`2 action)
   at System.Reactive.Linq.ObservableImpl.Delay`1.Base`1.L[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].ScheduleDrain()
   at System.Reactive.Linq.ObservableImpl.Delay`1.Relative.L[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].RunCore(Relative parent)
   at System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].Run(Relative parent)
   at System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Run(_ sink)
   at System.Reactive.Producer`2.<>c[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].<SubscribeRaw>b__1_0(ValueTuple`2 tuple)
   at System.Reactive.Concurrency.Scheduler.<>c__75`1[[System.ValueTuple`2[[System.Reactive.Producer`2[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].<ScheduleAction>b__75_0(IScheduler _, ValueTuple`2 tuple)
   at System.Reactive.Concurrency.CurrentThreadScheduler.Schedule[ValueTuple`2](ValueTuple`2 state, TimeSpan dueTime, Func`3 action)
   at System.Reactive.Concurrency.LocalScheduler.Schedule[ValueTuple`2](ValueTuple`2 state, Func`3 action)
   at System.Reactive.Concurrency.Scheduler.ScheduleAction[ValueTuple`2](IScheduler scheduler, ValueTuple`2 state, Action`1 action)
   at System.Reactive.Producer`2[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].SubscribeRaw(IObserver`1 observer, Boolean enableSafeguard)
   at System.Reactive.Producer`2[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Base`1._[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Reactive.Linq.ObservableImpl.Delay`1.Relative[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]], System.Reactive, Version=6.0.0.0, Culture=neutral, PublicKeyToken=94bc3704cddfc263]].Subscribe(IObserver`1 observer)
   at System.Reactive.Threading.Tasks.TaskObservableExtensions.ToTask[Int32](IObservable`1 observable, CancellationToken cancellationToken, Object state)
--- End of stack trace from previous location ---
   at TabletsUI.Pages.Index.OnInitializedAsync() in ..\Pages\Index.razor:line 11
   at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
idg10 commented 11 months ago

The issue is slightly deeper than this. Observable.Delay itself isn't the problem—it can be made to work. Here's a modification that works around the issue you're seeing:

IScheduler sch = DefaultScheduler.Instance.DisableOptimizations();
await Observable.Return(1)
    .Delay(TimeSpan.FromSeconds(1), sch)
    .ToTask();

You should then see Delay work correctly. This demonstrates that Delay itself is perfectly capable of working on Blazor WASM.

So the question is why does it fail when used in the default way?

The basic problem here is this: by default all timed operators in Rx default to using SchedulerDefaults.TimeBasedOperations to run work at the necessary time. That property returns an instance of DefaultScheduler, and DefaultScheduler supports an optional scheduler feature: ISchedulerLongRunning which it implements by spinning up a new thread. Delay will use this scheduler feature if available, because the nature of Delay (an inherently very expensive operator, and not one you would use if your only goal was to generate a single notification 1 second from now) is that it can work a lot better if it has a thread to itself.

The problem of course is that you're not allowed to create new threads on WASM. The workaround above basically downgrades the scheduler to one where no optional features (such as long running operation support) are available. This is probably overkill. This may be disabling other optimizations that would actually work.

It might be that the most straightforward way for us to fix this is to have the ConcurrencyAbstractionLayerImpl detect when threads can't be created. It currently has this:

public bool SupportsLongRunning => true;

but if this were to return false on Blazor WASM, you would no longer need the workaround, because the DefaultScheduler checks that property and only tries to create new threads if it returns true.

There was a time where different builds of Rx for different environments would hard-code SupportsLongRunning either to true or false because some of our build targets just didn't support creation of new threads at all.

The problem for WASM is that it's not a distinct build target. Because of the "One .NET" initiative, it just ends up being net6.0.

Presumably there's some way to discover at runtime that thread creation is unavailable—other libraries must have encountered this problem on WASM before. So it might just be a case of finding out what the preferred way is to ask "Are we allowed to create new threads?" and making SupportsLongRunning report that.

However, there might well be other WASM-specific problems. The root cause of why it was possible for this to blow up like this is that we've never executed our test suites on WASM.

So rather than merely fixing up this one symptom, I think the best thing to do would be to work out how to run the full Rx test suite on Blazor WASM. That would reveal any other issues.

idg10 commented 10 months ago

Note to self: maybe https://devblogs.microsoft.com/dotnet/introducing-ms-test-runner/ could help with this?

HowardvanRooijen commented 10 months ago

The other suggestion is to use https://learn.microsoft.com/en-us/aspnet/core/client-side/dotnet-interop?view=aspnetcore-8.0 as a testing approach.