-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
[WinHTTP] Make concurrent IO check thread safe #111750
Conversation
Tagging subscribers to this area: @dotnet/ncl |
/azp run runtime-libraries-coreclr outerloop-windows |
Azure Pipelines successfully started running 1 pipeline(s). |
src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseStream.cs
Show resolved
Hide resolved
@@ -221,12 +218,19 @@ private async Task<int> ReadAsyncCore(byte[] buffer, int offset, int count, Canc | |||
{ | |||
return 0; | |||
} | |||
|
|||
_state.PinReceiveBuffer(buffer); | |||
var ctr = token.Register(s => ((WinHttpResponseStream)s!).CancelPendingResponseStreamReadOperation(), this); |
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.
If there is a read in progress, and we now throw an exception outside of the try block, this will leak into the cancellation token.
/azp run runtime-libraries-coreclr outerloop-windows |
Azure Pipelines successfully started running 1 pipeline(s). |
if (Interlocked.CompareExchange(ref _state.AsyncReadInProgress, 1, 0) == 1) | ||
{ | ||
throw new InvalidOperationException(SR.net_http_no_concurrent_io_allowed); | ||
} |
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.
We'll end up not returning the rented buffer in this case, but that's fine; this only happens in incorrectly structured code, and in general we're ok with array pool buffers not being returned in exceptional situations.
@@ -357,7 +351,7 @@ private void CancelPendingResponseStreamReadOperation() | |||
{ | |||
lock (_state.Lock) | |||
{ | |||
if (_state.AsyncReadInProgress) | |||
if (_state.AsyncReadInProgress == 1) |
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 confused by the comment on this function. What happens if we Dispose of the handle even if there's not a pending request? There are race conditions here, both with and before this change.
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.
Honestly, I don't understand it either (it's been there since 2016: 87cf76a).
If we close the handle, all subsequent calls on it will obviously blow up with ODE or some such exception from the invalid handle.
Before this change, we set the AsyncReadInProgress
after the token registration. So the only thing that comes to mind is to prevent pre-cancelled token to kill the handle if the read itself hasn't started yet. But I'm grasping at straws here... I can remove that part of the comment.
One thing I noticed though is that for the sending side, we're not locking around the handle dispose (upon token cancellation) which we should. All calls to WinHTTP for one request handle should be serialized. So I'll fix that as well.
@@ -46,6 +46,29 @@ public void SendAsync_SimpleGet_Success() | |||
} | |||
} | |||
|
|||
[OuterLoop("Uses external server.")] | |||
[Fact] | |||
public async Task GetAsync_ConcurrentRead_ThrowsInvalidOperationException() |
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 unclear what this test is validating. Without your changes, won't this still generally pass? Is this a stress test masquerading as a unit 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.
Without your changes, won't this still generally pass?
Nope, I'm getting asserts in the awaitable:
Process terminated. Assertion failed.
!_resultSet && _error == null
at System.Threading.Tasks.RendezvousAwaitable`1.AssertResultConsistency(Boolean expectedCompleted) in C:\Users\mapichov\Repositories\runtime\src\libraries\Common\src\System\Threading\Tasks\RendezvousAwaitable.cs:line 149
at System.Threading.Tasks.RendezvousAwaitable`1.SetResult(TResult result) in C:\Users\mapichov\Repositories\runtime\src\libraries\Common\src\System\Threading\Tasks\RendezvousAwaitable.cs:line 79
at System.Net.Http.WinHttpRequestCallback.OnRequestDataAvailable(WinHttpRequestState state, Int32 bytesAvailable) in C:\Users\mapichov\Repositories\runtime\src\libraries\System.Net.Http.WinHttpHandler\src\System\Net\Http\WinHttpRequestCallback.cs:line 146
at System.Net.Http.WinHttpRequestCallback.RequestCallback(WinHttpRequestState state, UInt32 internetStatus, IntPtr statusInformation, UInt32 statusInformationLength) in C:\Users\mapichov\Repositories\runtime\src\libraries\System.Net.Http.WinHttpHandler\src\Syste
m\Net\Http\WinHttpRequestCallback.cs:line 70
at System.Net.Http.WinHttpRequestCallback.WinHttpCallback(IntPtr handle, IntPtr context, UInt32 internetStatus, IntPtr statusInformation, UInt32 statusInformationLength) in C:\Users\mapichov\Repositories\runtime\src\libraries\System.Net.Http.WinHttpHandler\src\S
ystem\Net\Http\WinHttpRequestCallback.cs:line 46
as well as unexpected exception (I had to narrow down the catch block to filter only concurrent IO errors):
System.InvalidOperationException : Handle is not initialized.
Stack Trace:
at System.Runtime.InteropServices.GCHandle.Free()
C:\Users\mapichov\Repositories\runtime\src\libraries\System.Net.Http.WinHttpHandler\src\System\Net\Http\WinHttpRequestState.cs(168,0): at System.Net.Http.WinHttpRequestState.PinReceiveBuffer(Byte[] buffer)
C:\Users\mapichov\Repositories\runtime\src\libraries\System.Net.Http.WinHttpHandler\src\System\Net\Http\WinHttpResponseStream.cs(225,0): at System.Net.Http.WinHttpResponseStream.<ReadAsyncCore>d__24.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ValueTaskAwaiter`1.GetResult()
C:\Users\mapichov\Repositories\runtime\src\libraries\System.Net.Http.WinHttpHandler\tests\FunctionalTests\WinHttpHandlerTest.cs(49,0): at System.Net.Http.WinHttpHandlerFunctional.Tests.WinHttpHandlerTest.<>c__DisplayClass3_0.<<GetAsync_ConcurrentRead_Throws
InvalidOperationException>b__0>d.MoveNext()
The test is in outerloop, so it won't run with each PR. But I can remove the test, I'm not insisting on having it.
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's what I meant about it being a stress test, though. It's trying to inflict certain interleavings by running a lot. If it's reasonably fast, though, OK.
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.
~ 0.3-0.5 s in our CI, not the fastest, but we have much slower tests in outerloop running for 1-3s.
/azp run runtime-libraries-coreclr outerloop-windows |
Azure Pipelines successfully started running 1 pipeline(s). |
/azp run runtime-libraries-coreclr outerloop-windows |
Azure Pipelines successfully started running 1 pipeline(s). |
Outerloop failures unrelated. |
Note that this change makes the
InvalidOperationException
for concurrent IO async from sync. So now it's stored in the returnedTask
instead of being thrown synchronously.Also, I can change this to
Interlocked.CompareExchange
. I went with the straight forward solution, for compare exchange, I'd have to changeAsyncReadInProgress
to either a method or a public field.