dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.89k stars 9.85k forks source link

MaximumParallelInvocationsPerClient > 1 breaks file upload in Blazor Server mode #53951

Open seqai opened 5 months ago

seqai commented 5 months ago

Is there an existing issue for this?

Describe the bug

With Blazor Server configured like this:

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents()
    .AddHubOptions(o => o.MaximumParallelInvocationsPerClient = 2);

When trying to upload a file using IBrowserFile from, for example, standard InputFile component and using standard library Stream.CopyTo

There's a very high probability, that CopyTo operation might throw System.InvalidOperationException: 'Reading is not allowed after reader was completed.' exception. For me it happens more often that not.

Expected Behavior

I would expect the stream to be copied normally.

Alternatively, if this is a known limitation or a feature of client parallelism in Blazor I believe the documentation for MaximumParallelInvocationsPerClient should state this.

Steps To Reproduce

Please find a minimal reproduction repo here: https://github.com/seqai/aspnetcore_MaximumParallelInvocationsPerClient_issue

With smaller files, it seems, it is possible to get "lucky" and upload the file. With large files, i.e. several megabytes, I encounter the exception always.

We also encountered this issue in an actual deployment environment in docker.

Exceptions (if any)

System.InvalidOperationException HResult=0x80131509 Message=Concurrent reads or writes are not supported. Source=System.IO.Pipelines StackTrace: at System.IO.Pipelines.Pipe.GetFlushAsyncResult() at Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSDataStream.d18.MoveNext() at System.Threading.Tasks.ValueTask1.get_Result() at System.Runtime.CompilerServices.ValueTaskAwaiter1.GetResult() at Microsoft.AspNetCore.Components.Server.Circuits.RemoteJSDataStream.d__36.MoveNext() at System.Threading.Tasks.ValueTask1.get_Result() at System.Runtime.CompilerServices.ValueTaskAwaiter1.GetResult() at Microsoft.AspNetCore.Components.Forms.BrowserFileStream.d30.MoveNext() at System.Threading.Tasks.ValueTask1.get_Result() at System.Runtime.CompilerServices.ValueTaskAwaiter1.GetResult() at Microsoft.AspNetCore.Components.Forms.BrowserFileStream.d28.MoveNext() at System.IO.Stream.<gCore|27_0>d.MoveNext() at FileUploadSample.FileService.d__0.MoveNext() in C:\Users\ivan\source\repos\FileUploadSample\FileService.cs:line 16

This exception was originally thrown at this call stack: [External Code] FileUploadSample.FileService.UploadFileAsync(Microsoft.AspNetCore.Components.Forms.IBrowserFile) in FileService.cs

.NET Version

8.0.100

Anything else?

I also found an issue where changing MaximumParallelInvocationsPerClient is a proposed workaround for problems with multiple upload of files: https://github.com/dotnet/aspnetcore/issues/47301

In my case it seems that this property, on contrary, seems to bring problems. The issue is encountered in management panel with very low usage while I was investigating possible ways to get a bit more performance for our user.

mkArtakMSFT commented 1 month ago

@guardrex can you please make a note in docs that Blazor relies on the MaximumParallelInvocationsPerClient to be 1.

mkArtakMSFT commented 1 month ago

@seqai out of curiosity, can you share more details about why setting it to more than 1 is important to you?

seqai commented 1 week ago

@seqai out of curiosity, can you share more details about why setting it to more than 1 is important to you?

Hi, sorry for late reply. As I stated in the original issue, the issue is encountered in the internal data management panel with a very low usage (2 administrators uploading and managing data every evening on a daily basis) while I was investigating possible ways to get a bit more performance for our users. Uploading and processing larger files seem to make application a bit unresponsive for some amount of time (up to a minute for larger data sets).

This application is not a very elegantly engineered tool, rather a quick internal solution. I can eventually extract heavier processing into a separate service, but it wasn't and still is not very critical.

Overall, I would just like to know what settings are safe and not safe to use. And I see that a remark was added to https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/signalr?view=aspnetcore-8.0, so it should save some time for the people in the similar situation.