-
Notifications
You must be signed in to change notification settings - Fork 10.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Streaming Interop Followup Items #33916
Conversation
* CI Debugging * CiData message * CiData message * Update RemoteJSDataStream.cs * Remove Task.Delay * Update RemoteJSDataStream.cs * Update RemoteJSDataStream.cs
long pauseIncomingBytesThreshold, | ||
long resumeIncomingBytesThreshold, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per David Fowler's suggestion #33491 (comment)
@@ -36,7 +36,7 @@ static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JS | |||
static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask | |||
*REMOVED*static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, params object![]! args) -> System.Threading.Tasks.ValueTask | |||
static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask | |||
virtual Microsoft.JSInterop.JSRuntime.ReadJSDataAsStreamAsync(Microsoft.JSInterop.IJSStreamReference! jsStreamReference, long totalLength, long maxBufferSize, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<System.IO.Stream!>! | |||
virtual Microsoft.JSInterop.JSRuntime.ReadJSDataAsStreamAsync(Microsoft.JSInterop.IJSStreamReference! jsStreamReference, long totalLength, long pauseIncomingBytesThreshold, long resumeIncomingBytesThreshold, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<System.IO.Stream!>! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a big fan of this API.
It exposes a bunch of internal details and it does so in a way that is problematic and that can make it easy to introduce security issues if you are not careful. The defaults need to be safe (the default can't never be "unlimited" on a server app) and we should add a remark on the comments about the security implications of these parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@javiercn Would you consider it sufficient to:
- have some default like 100KB or so
- remove the behavior of "zero means unlimited" so that if someone really wants unlimited they have to pass
int.MaxValue
or similar - include a mention in the XML docs for the parameter saying something like
Avoid specifying an excessively large value because this could allow clients to exhaust server memory.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds about right.
We would need to figure out a good default. It's a trade-off between speed and availability I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify this here, we do have a safe default (indicated by -1
, zero is not the default). Users should be working with the JSStreamReference
and not the JSRuntime
methods seen here. This follows the exact pattern seen in the PipeOptions
, and we utilize these parameters directly with the Pipe
during initialization.
What the safe defaults translate to:
https://github.com/dotnet/runtime/blob/9fb6e818cda2e4364e085fe0b8ecebed97db9596/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/PipeOptions.cs#L47-L69
(65k and 32.5k bytes). Originally we were using a 100kb but per #33491 (comment), it seemed like using the Pipe
defaults was preferred.
Are we okay keeping with this general approach in this case? Action items:
- Update the comments to provide more clarity on -1 and security implications of using large sizes
- Reflect the safe defaults
-1
in theJSRuntime
as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would 7785751 resolve these concerns?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/ping
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few questions that I've put on the comments.
In addition to those questions, I would like us to add an API that exposes the PipeReader directly. This essentially saves an intermediate copy from the Stream. Look for other APIs that expose the PipeReader and follow the same pattern.
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
I don't really mind, and am fine with requiring people to supply a typed array. We can always add support for more cases in the future if we want. However for clarity, an arraybuffer is just a store of bytes that can be viewed as being one of several possible types (Uint8Array, Uint32Array, etc.). So if we receive an arraybuffer, it's always correct and valid to read the bytes directly from it using any element size we choose arbitrarily. For example we could freely |
jsStreamReference, | ||
totalLength: 15, | ||
maximumIncomingBytes: 10_000, | ||
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(30), // Note we're using a 30 second timeout for this test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally we try to make tests as quick as possible, we do this by
a) not using real timers (mock timers so we control when they tick and how much time they think has passed)
b) avoiding timers if possible (presumably this test is actually testing a timeout so b. doesn't apply)
c) using much lower values for timeouts if a. or b. can't be done
Additionally, relying on a real timeout creates potential flakiness because what if the machine is really slow and the test doesn't do the work you expected before the timeout occurs?
Couple examples:
aspnetcore/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TimeoutTests.cs
Lines 26 to 35 in cee22ed
var mockSystemClock = _serviceContext.MockSystemClock; | |
var limits = _serviceContext.ServerOptions.Limits; | |
_timeoutControl.Initialize(mockSystemClock.UtcNow.Ticks); | |
CreateConnection(); | |
_connectionTask = _connection.ProcessRequestsAsync(new DummyApplication(_noopApplication)); | |
AdvanceClock(limits.KeepAliveTimeout + Heartbeat.Interval); |
aspnetcore/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs
Lines 2690 to 2708 in 65276b0
var clock = new MockSystemClock(); | |
var serviceProvider = HubConnectionHandlerTestUtils.CreateServiceProvider(services => | |
services.Configure<HubOptions>(options => | |
options.KeepAliveInterval = TimeSpan.FromMilliseconds(interval)), LoggerFactory); | |
var connectionHandler = serviceProvider.GetService<HubConnectionHandler<MethodHub>>(); | |
connectionHandler.SystemClock = clock; | |
using (var client = new TestClient(new NewtonsoftJsonHubProtocol())) | |
{ | |
var connectionHandlerTask = await client.ConnectAsync(connectionHandler); | |
await client.Connected.DefaultTimeout(); | |
// Trigger multiple keep alives | |
var heartbeatCount = 5; | |
for (var i = 0; i < heartbeatCount; i++) | |
{ | |
clock.UtcNow = clock.UtcNow.AddMilliseconds(interval + 1); | |
client.TickHeartbeat(); | |
} |
Last thing, when using await
you should consider putting .DefaultTimeout()
on the calls so that any hangs will fail the test (and tell you where they hung) and not timeout the test run instead.
Example:
aspnetcore/src/SignalR/server/SignalR/test/HubConnectionHandlerTests.cs
Lines 4453 to 4461 in 65276b0
await client.Connected.OrThrowIfOtherFails(connectionHandlerTask).DefaultTimeout(); | |
await client.BeginUploadStreamAsync("invocation", nameof(MethodHub.UploadDoesWorkOnComplete), streamIds: new[] { "id" }, args: Array.Empty<object>()).DefaultTimeout(); | |
await client.SendHubMessageAsync(new StreamItemMessage("id", "hello")).DefaultTimeout(); | |
await client.DisposeAsync().DefaultTimeout(); | |
await connectionHandlerTask.DefaultTimeout(); |
Sorry if this ended up looking like a bit of a rant, but I really want our tests to be reliable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @BrennanConroy appreciate the feedback. I went the route of using a TaskCompletionSource
to match the pattern used in other src/Components/tests
tests to keep consistency. I also significantly reduced the timeout values to avoid long running tests, and the TaskCompletionSource
approach should eliminate flakiness as we'll be able to await the timeout. I've also updated the tests to leverage DefaultTimeout()
.
Will expose a public Edit: Done in 94ea801 |
I'll update in #33900 as that area is already being reworked in #33900. Edit: Done here. |
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
@javiercn @SteveSandersonMS could I please get a review, hoping to get it in for Preview 7. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolves:
JS
toDotNet
#33491 (comment)JS
toDotNet
#33491 (comment)ArrayBuffer
s directly: Blazor Streaming Interop |JS
toDotNet
#33491 (comment)ArrayBuffer
's aren't normally used directly and we don't have element access, so I don't think we need to directly support them. I've updated the error message in the original PR.JS
toDotNet
#33491 (comment)Separate PR:
JS
toDotNet
#33491 (comment)