Skip to content

Make StartAsync not throw if we haven't started the response #8199

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

Merged
merged 16 commits into from
Mar 8, 2019

Conversation

jkotalik
Copy link
Contributor

@jkotalik jkotalik commented Mar 5, 2019

For #7779.

One scenario this doesn't support is calling StartAsync after calling GetMemory and using the memory immediately after. @halter73 and I deemed it too difficult to fix well and is an incredibly niche scenario.

public async Task AppFunc(HttpContext context)
{
    var memory = context.Response.BodyPipe.GetMemory();
    await context.Response.StartAsync();
    // Try using the memory here, you'll get an ODE.
    // Advance will also 
}

I added a check that would see if Advance was called immediately after calling StartAsync and throw if so.

@jkotalik
Copy link
Contributor Author

jkotalik commented Mar 5, 2019

@anurse test discovery issue?

[xUnit.net 00:00:05.78] Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests: Exception thrown during theory discovery on 'Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.RedisEndToEndTests.HubConnectionCanSendAndReceiveMessages'; falling back to single test case.
System.InvalidOperationException: No process is associated with this object.
   at System.Diagnostics.Process.EnsureState(State state)
   at System.Diagnostics.Process.EnsureState(State state)
   at System.Diagnostics.Process.GetWaitState()
   at System.Diagnostics.Process.WaitForExitCore(Int32 milliseconds)
   at System.Diagnostics.Process.WaitForExit()
   at Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.Docker.RunProcessAndWait(String fileName, String arguments, String prefix, ILogger logger, TimeSpan timeout, String& output) in /_/src/SignalR/server/StackExchangeRedis/test/Docker.cs:line 163
   at Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.Docker.RunCommand(String commandAndArguments, String prefix, ILogger logger, String& output) in /_/src/SignalR/server/StackExchangeRedis/test/Docker.cs:line 135
   at Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.Docker.RunCommand(String commandAndArguments, String prefix, String& output) in /_/src/SignalR/server/StackExchangeRedis/test/Docker.cs:line 131
   at Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.Docker.Create() in /_/src/SignalR/server/StackExchangeRedis/test/Docker.cs:line 44
   at System.Lazy`1.ViaFactory(LazyThreadSafetyMode mode)
   at System.Lazy`1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
   at System.Lazy`1.CreateValue()
   at System.Lazy`1.get_Value()
   at Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.Docker.get_Default() in /_/src/SignalR/server/StackExchangeRedis/test/Docker.cs:line 23
   at Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.SkipIfDockerNotPresentAttribute.CheckDocker() in /_/src/SignalR/server/StackExchangeRedis/test/SkipIfDockerNotPresentAttribute.cs:line 17
   at Microsoft.AspNetCore.SignalR.StackExchangeRedis.Tests.SkipIfDockerNotPresentAttribute.get_IsMet() in /_/src/SignalR/server/StackExchangeRedis/test/SkipIfDockerNotPresentAttribute.cs:line 12
   at Microsoft.AspNetCore.Testing.xunit.TestMethodExtensions.EvaluateSkipConditions(ITestMethod testMethod)
   at Microsoft.Extensions.Logging.Testing.LoggedConditionalTheoryDiscoverer.CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, Object[] dataRow)
   at Xunit.Sdk.TheoryDiscoverer.Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) in C:\Dev\xunit\xunit\src\xunit.execution\Sdk\Frameworks\TheoryDiscoverer.cs:line 238

@jkotalik jkotalik assigned jkotalik and unassigned jkotalik Mar 5, 2019
@BrennanConroy
Copy link
Member

Didn't realize this was failing builds, should be fixed with #8201

@analogrelay
Copy link
Contributor

/azp run AspNetCore-helix-test

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jkotalik
Copy link
Contributor Author

jkotalik commented Mar 5, 2019

@JamesNK and @JunTaoLuo are you okay with the restrictions on StartAsync that I listed above? After calling StartAsync, don't use the memory that was obtained before calling StartAsync.

@@ -1066,7 +1072,7 @@ private HttpResponseHeaders CreateResponseHeaders(bool appCompleted)
{
_keepAlive = false;
}
else if (appCompleted || !_canWriteResponseBody)
else if ((appCompleted || !_canWriteResponseBody) && !_hasAdvanced) // Avoid setting contentLength of 0 if we wrote data before calling CreateResponseHeaders
Copy link
Member

Choose a reason for hiding this comment

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

if appCompleted && _hasAdvanced then we can set ContentLength to a specific value and avoid chunking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I was thinking about doing this in this PR but wasn't a fan. You would need to peek into the number of advanced bytes inside of the Http1OutputProducer which isn't clean. I'd prefer to do it in a separate pr.

@JunTaoLuo
Copy link
Contributor

JunTaoLuo commented Mar 5, 2019

@jkotalik Yes we are fine with that restriction, We currently only call GetSpan after calling StartAsync. We plan to remove the StartAsync calls once this PR is merged so the case of calling GetSpan/GetMemory before StartAsync will not occur.

@JamesNK
Copy link
Member

JamesNK commented Mar 5, 2019

@JamesNK and @JunTaoLuo are you okay with the restrictions on StartAsync that I listed above? After calling StartAsync, don't use the memory that was obtained before calling StartAsync.

I'm pretty sure that is fine. I'll take a closer look tomorrow.

@jkotalik
Copy link
Contributor Author

jkotalik commented Mar 5, 2019

@JamesNK @JunTaoLuo the only thing I'm concerned about is how you mentioned that you wanted to avoid calling StartAsync to have buffering for free so you can modify headers later. It may cause issues if you call StartAsync later, putting you in a weird state.

@JamesNK
Copy link
Member

JamesNK commented Mar 6, 2019

We wouldn't call StartAsync while in the middle of writing to Memory<byte>. The scenario we have is writing to the span/memory and calling Advance, then later calling StartAsync.

@JunTaoLuo
Copy link
Contributor

then later calling StartAsync.
@JamesNK I think you are thinking of WriteResponseHeadersAsyncCore which calls FlushAsync: https://github.com/grpc/grpc-dotnet/blob/master/src/Grpc.AspNetCore.Server/Internal/HttpContextServerCallContext.cs#L201.

Or did you mean something else?

@jkotalik jkotalik force-pushed the jkotalik/StartAsyncSimple branch from e75165f to da8b6ed Compare March 6, 2019 19:34
// If we are writing with GetMemory/Advance before calling StartAsync, assume we can write and throw away contents if we can't.
private bool _canWriteResponseBody = true;
private bool _hasAdvanced;
private bool _isLeasedMemoryInvalid = true;
Copy link
Member

Choose a reason for hiding this comment

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

Should _hasAdvanced and _isLeasedMemoryInvalid be reset between requests?

Copy link
Member

Choose a reason for hiding this comment

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

Is there a regression test that will fail if we forgot to reset?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya caught me.

@jkotalik
Copy link
Contributor Author

jkotalik commented Mar 6, 2019

🆙 📅

@@ -49,7 +49,6 @@ public static async Task EchoAppPipeWriter(HttpContext httpContext)
if (buffer.Length > 0)
{
await request.Body.ReadUntilEndAsync(buffer).DefaultTimeout();
await response.StartAsync();
Copy link
Member

@halter73 halter73 Mar 6, 2019

Choose a reason for hiding this comment

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

I don't think we should have unrelated tests hit the inefficient code path. Tests like ChunkGetMemoryAndWriteWithoutStart should be sufficient.

@@ -534,6 +579,8 @@ public async Task ChunksWithGetMemoryWithInitialFlushWorks()
{
var response = httpContext.Response;

await response.StartAsync();

Copy link
Member

Choose a reason for hiding this comment

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

The old version of this test would still pass, right?

await response.StartAsync();

var memory = response.BodyWriter.GetMemory();
length.Value = memory.Length - 1;
Copy link
Member

Choose a reason for hiding this comment

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

FYI: if length was a normal int, this test would still work since the closure would box it.

@@ -3088,7 +3174,7 @@ public async Task ContentLengthWithGetMemoryWorks()
{
var response = httpContext.Response;
response.ContentLength = 12;
await response.StartAsync();
await Task.CompletedTask;
Copy link
Member

Choose a reason for hiding this comment

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

Nit

@@ -2681,7 +2727,6 @@ public async Task WriteAsync_GetMemoryLargeWriteBeforeFirstFlush()
await InitializeConnectionAsync(async httpContext =>
{
var response = httpContext.Response;
await response.StartAsync();
Copy link
Member

Choose a reason for hiding this comment

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

If the test isn't explicitly about omitting StartAsync, and calling StartAsync is more efficient, call StartAsync.

Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

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

Pending a few nits. I'd like a test verifying _isLeasedMemoryInvalid is reset for example.

@jkotalik
Copy link
Contributor Author

jkotalik commented Mar 7, 2019

Talked offline, there is actually a good amount of missed scenarios here, especially with resetting state in the Http1OutputProducer. I'll take a look at how to refactor this to avoid this issue in the future.

@jkotalik
Copy link
Contributor Author

jkotalik commented Mar 7, 2019

@halter73 I added a reset method to HttpOutputProducer. All other ways weren't clean.

@jkotalik jkotalik merged commit bae2f22 into master Mar 8, 2019
@jkotalik jkotalik deleted the jkotalik/StartAsyncSimple branch March 8, 2019 02:32
@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants