Aspnetcore: [Server side] Doing a blocking call on IJSRuntime.InvokeAsync ends up locking the server

Created on 25 Jul 2018  ·  22Comments  ·  Source: dotnet/aspnetcore

Super nitpicky, but doing a .Result or 'Wait on the task returned by IJSRuntime locks up the server. E.g.

C# void DoLog() { JSRuntime.Current.InvokeAsync<string>("console.log", "Hello world").Wait(); }

The server does not respond to any further actions.

area-blazor bug

Most helpful comment

Hi,
try to use it
string tnkn=(JSRuntime.Current as IJSInprocess.Runtime).Invoke("GetTkn",...)
I used it and it worked for me in synchronous code. but this is not my personal hack. I saw this in some of the Blazors extensions unfortunately I don’t remember in which

All 22 comments

Correction: It locks up the current connection. New connections continue work.

Is this the correct way to synchronously execute async? I thought you should call .GetAwaiter().GetResult(); ?

Same when calling .Result; no error message no error is showing, application doesn't die but freezes, doesn't do anything anymore. Not only by a IJSRuntime, any call to .Result freezes the app.

Just had something similar on my chat sample app, https://github.com/conficient/BlazorChatSample

I was upgrading to use async invoke, and it was hanging and not returning when I called

var tmp = await JSRuntime.Current.InvokeAsync<bool>("ChatClient.Start", ....`

The JS method returns the promise returned by SignalR's connection.start() method, but this wasn't working. The method returns a promise but there isn't a return value in the promise (at least one I don't need).

Changing this to

var tmp = await JSRuntime.Current.InvokeAsync<object>("ChatClient.Start", ...

fixed the problem and it worked, so I suggest you try that. I also amended my C# methods to be async as well (see https://github.com/conficient/BlazorChatSample/blob/master/BlazorChatSample.Client/ChatClient.cs )

Making a blocking call on a task can deadlock in ASP.NET too, this is because of synchronization context, I recommend using await and not blocking at all if possible.

@pranavkm as @psibernetic said, blocking asynchronous calls in code running on any server-side ASP.NET Core stack will cause a deadlock; however, if you are unable to await the call, you can get it's result by using Task.Run(async () => await JSRuntime.Current.InvokeAsync<string>("console.log", "Hello world").ConfigureAwait(false)).Result, but it may or may not launch the call into a new thread. As far as I know, that should allow for the blocking of a thread for the purposes of synchronously waiting for the result of an asynchronous call, without deadlocking the process. Please let me know if it works.

@TheFanatr the bug was discovered \ filed as part of verifying the 0.5.0 release. Not doing blocking calls are always recommended, but oftentimes developers or even frameworks write code that blocks. The current experience around it isn't super great.

We need to do something to address this even if it's throwing an exception to report the deadlock. Threads are a limited resource.

Shouldn't it be possible to easily fix this just by adding ConfigureAwait(false) to the awaited async calls in the InvokeAsync method itself to avoid deadlocks?

@pranavkm as @psibernetic said, blocking asynchronous calls in code running on any server-side ASP.NET Core stack will cause a deadlock

No absolutely not. This deadlock is blazor specific behavior.

A custom awaiter backed by ValueTask may be the best way to achieve this.

I have the same problem with 0.5.1.
My scenario: I get the JWT token from the local storage in my App.cshtml (this is always async as js interop operations are only async). This operation should complete before OnInitAsync ends because pages and child components need this token for authenticating the API calls.

P.S. None of the workarounds suggested in this issue worked. I ended up getting the token before every request as a workaround until this issue is resolved. 😃

@NikolayIT Does awaiting the async call not work for your scenario?

@danroth27 awaiting the async call works as expected. But as I explained I need to execute it synchronously as its result is needed by the child components and pages.

If you are interested, I can add you to my repo so you can see the code there.

@NikolayIT Ah, I see. Yes, even if you await the call to retrieve the JWT in OnInitAsync the component rendering will proceed. You'll need to have logic in the child components that checks to see if the JWT is available, and if not display some sort of "Loading..." message. We do rerender once OnInitAsync completes. If your setting up the JWT in the App component and using routing, then that complicates things further. We probably should have better docs on how to do this: https://github.com/aspnet/Blazor.Docs/issues/240

Hi,
try to use it
string tnkn=(JSRuntime.Current as IJSInprocess.Runtime).Invoke("GetTkn",...)
I used it and it worked for me in synchronous code. but this is not my personal hack. I saw this in some of the Blazors extensions unfortunately I don’t remember in which

@SteveSandersonMS - I'm looking into fixing this. This would be cleanest to solve if DotNetDispatcher.EndInvoke were an intrinsic rather than just another usage of JS Interop. Put another way, the fact that DotNetDispatcher.EndInvoke is called via JS Interop by the client means that it's functionally indistinguishable from other JS Interop calls.

The problem with this is that I need to be able to break the rules when completing an interop operation. Interop calls need to acquire the sync context in order to call user code. In this case acquiring the sync context is what causes the deadlock.


I also tried implementing detection for deadlocks instead, but it ends up being a very hardcore solution. Basically I can make it so that the sync context is notified whenever blocking occurs, but that includes all blocking, locks, events, tasks, etc - http://joeduffyblog.com/2008/02/27/hooking-clr-blocking-calls-with-synchronizationcontext/

We could pursue a solution like this, but it means that our JS interop layer (including SignalR) must never acquire a lock. I think it's not something we could really do.


Here's what I'd like to do. Change DotNetDispatcher.EndInvoke to not be JS Invokable, and make it another method exposed by DotNetCallDispatcher - then replace the usage of it in the .ts code with the direct call.

The circuit host will then set JSRuntime.Current but not acquire the sync context before calling EndInvoke.

@rynowak why not detect and throw, why "fix" it?

What's your suggestion on how to detect a task.Result but not a random lock? Using SynchronizationContext.Wait seems like too big of a hammer.

Is there any chance of including the fix in preview6 or preview7?

This bug is still present rigth?

I've found this thread using LocalStorage from BlazorExtensions.Storage.

Just for the records, this experiment:

async void IncrementCount()
    {
        var result = 0;
        var t = Task.Run(async () => { result = await localStorage.GetItem<int>("counter"); }).ConfigureAwait(false);
        while (!t.GetAwaiter().IsCompleted)
        {
            Task.Delay(100);
        }
       ........

IsCompleted is always false so this never ends

Actually to understand what happen when trying to launch a task on Blazor you should understand JavaScript execution lifecycle (as Blazor pages are compiled on some g.code file which seems to be some JavaScript execution context fork (WebAssembly ??))

To sum up, any task in a JavaScript environment in the context of the browser (Safari, Chrome , Edge, ... and some server implementation too : NodeJS) cannot be executed when the current task is running. That means you are dead locking when trying to wait the initiated task by JSRuntime.InvokeAsync<...>(“functionName”,...).AsTask().Wait() in a “synchronous” Blazor function. Because the task you initiated and you wait for is waiting that your current task is ended to be started by the task scheduler.

The solution is to always perform those task calls in an async containing function and to not wait.

The recommendation here is to not perform blocking calls. It's fairly likely the work that was suggested in https://github.com/dotnet/aspnetcore/issues/5553#issuecomment-437714763 has been addressed since JSInterop uses a separate hub method to invoke. In Wasm, you get a nice error if you perform a blocking call. Closing this, as there isn't any further action to take here.

Was this page helpful?
0 / 5 - 0 ratings