Skip to content
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

Merged
merged 7 commits into from
Jul 9, 2021
Merged

Conversation

TanayParikh
Copy link
Contributor

@TanayParikh TanayParikh commented Jun 28, 2021

Resolves:

Separate PR:

@TanayParikh TanayParikh added the area-blazor Includes: Blazor, Razor Components label Jun 28, 2021
* CI Debugging

* CiData message

* CiData message

* Update RemoteJSDataStream.cs

* Remove Task.Delay

* Update RemoteJSDataStream.cs

* Update RemoteJSDataStream.cs
@TanayParikh TanayParikh marked this pull request as ready for review June 29, 2021 05:57
@TanayParikh TanayParikh requested review from Pilchie and a team as code owners June 29, 2021 05:57
Comment on lines 45 to 46
long pauseIncomingBytesThreshold,
long resumeIncomingBytesThreshold,
Copy link
Contributor Author

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!>!
Copy link
Member

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.

Copy link
Member

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.

?

Copy link
Member

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

Copy link
Contributor Author

@TanayParikh TanayParikh Jun 29, 2021

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.

Documentation: https://github.com/dotnet/runtime/blob/9fb6e818cda2e4364e085fe0b8ecebed97db9596/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/PipeOptions.cs#L22-L23

Default Args:
https://github.com/dotnet/runtime/blob/9fb6e818cda2e4364e085fe0b8ecebed97db9596/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/PipeOptions.cs#L30-L31

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 the JSRuntime as well.

Copy link
Contributor Author

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ping

Copy link
Member

@javiercn javiercn left a 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.

@greenEkatherine
Copy link

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@SteveSandersonMS
Copy link
Member

Looked into this some more, it seems 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.

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 new Uint8Array(arrayBuffer) to get access to the contents as raw bytes.

jsStreamReference,
totalLength: 15,
maximumIncomingBytes: 10_000,
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(30), // Note we're using a 30 second timeout for this test
Copy link
Member

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:

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);

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:

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.

Copy link
Contributor Author

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().

1cd162f

@TanayParikh
Copy link
Contributor Author

TanayParikh commented Jun 29, 2021

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.

Will expose a public PipeReader property like we do here or here.

Edit: Done in 94ea801

@TanayParikh
Copy link
Contributor Author

TanayParikh commented Jun 29, 2021

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 new Uint8Array(arrayBuffer) to get access to the contents as raw bytes.

I'll update in #33900 as that area is already being reworked in #33900.

Edit: Done here.

@TanayParikh
Copy link
Contributor Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 2 pipeline(s).

@TanayParikh
Copy link
Contributor Author

@javiercn @SteveSandersonMS could I please get a review, hoping to get it in for Preview 7.

Copy link
Member

@SteveSandersonMS SteveSandersonMS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice - this looks good to me! Might be good to ask @javiercn if he is happy with all the resolutions here, but I know you've been waiting a while to merge this and we're getting close to the cutoff date, so it would probably be fine to merge and let @javiercn comment later if he needs :)

@TanayParikh TanayParikh merged commit 774b2af into main Jul 9, 2021
@TanayParikh TanayParikh deleted the taparik/streamingFollowups branch July 9, 2021 17:19
@ghost ghost added this to the 6.0-preview7 milestone Jul 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants