3
3
4
4
using System . Diagnostics ;
5
5
using System . Threading . Tasks . Sources ;
6
+ using Microsoft . Extensions . Logging ;
6
7
7
8
namespace Microsoft . AspNetCore . Server . HttpSys ;
8
9
9
- internal sealed unsafe class AsyncAcceptContext : IValueTaskSource < RequestContext > , IDisposable
10
+ internal sealed unsafe partial class AsyncAcceptContext : IValueTaskSource < RequestContext > , IDisposable
10
11
{
11
12
private static readonly IOCompletionCallback IOCallback = IOWaitCallback ;
12
13
private readonly PreAllocatedOverlapped _preallocatedOverlapped ;
13
14
private readonly IRequestContextFactory _requestContextFactory ;
15
+ private readonly ILogger _logger ;
16
+ private int _expectedCompletionCount ;
14
17
15
18
private NativeOverlapped * _overlapped ;
16
19
20
+ private readonly bool _logExpectationFailures = AppContext . TryGetSwitch (
21
+ "Microsoft.AspNetCore.Server.HttpSys.LogAcceptExpectationFailure" , out var enabled ) && enabled ;
22
+
17
23
// mutable struct; do not make this readonly
18
24
private ManualResetValueTaskSourceCore < RequestContext > _mrvts = new ( )
19
25
{
@@ -23,11 +29,12 @@ internal sealed unsafe class AsyncAcceptContext : IValueTaskSource<RequestContex
23
29
24
30
private RequestContext ? _requestContext ;
25
31
26
- internal AsyncAcceptContext ( HttpSysListener server , IRequestContextFactory requestContextFactory )
32
+ internal AsyncAcceptContext ( HttpSysListener server , IRequestContextFactory requestContextFactory , ILogger logger )
27
33
{
28
34
Server = server ;
29
35
_requestContextFactory = requestContextFactory ;
30
36
_preallocatedOverlapped = new ( IOCallback , state : this , pinData : null ) ;
37
+ _logger = logger ;
31
38
}
32
39
33
40
internal HttpSysListener Server { get ; }
@@ -50,15 +57,16 @@ internal ValueTask<RequestContext> AcceptAsync()
50
57
return new ValueTask < RequestContext > ( this , _mrvts . Version ) ;
51
58
}
52
59
53
- private void IOCompleted ( uint errorCode , uint numBytes )
60
+ private void IOCompleted ( uint errorCode , uint numBytes , bool managed )
54
61
{
55
62
try
56
63
{
64
+ ObserveCompletion ( managed ) ; // expectation tracking
57
65
if ( errorCode != ErrorCodes . ERROR_SUCCESS &&
58
66
errorCode != ErrorCodes . ERROR_MORE_DATA )
59
67
{
60
- _mrvts . SetException ( new HttpSysException ( ( int ) errorCode ) ) ;
61
- return ;
68
+ // (keep all the error handling in one place)
69
+ throw new HttpSysException ( ( int ) errorCode ) ;
62
70
}
63
71
64
72
Debug . Assert ( _requestContext != null ) ;
@@ -70,7 +78,14 @@ private void IOCompleted(uint errorCode, uint numBytes)
70
78
// we want to reuse the acceptContext object for future accepts.
71
79
_requestContext = null ;
72
80
73
- _mrvts . SetResult ( requestContext ) ;
81
+ try
82
+ {
83
+ _mrvts . SetResult ( requestContext ) ;
84
+ }
85
+ catch ( Exception ex )
86
+ {
87
+ Log . AcceptSetResultFailed ( _logger , ex ) ;
88
+ }
74
89
}
75
90
else
76
91
{
@@ -83,22 +98,69 @@ private void IOCompleted(uint errorCode, uint numBytes)
83
98
if ( statusCode != ErrorCodes . ERROR_SUCCESS &&
84
99
statusCode != ErrorCodes . ERROR_IO_PENDING )
85
100
{
86
- // someother bad error, possible(?) return values are:
101
+ // some other bad error, possible(?) return values are:
87
102
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
88
- _mrvts . SetException ( new HttpSysException ( ( int ) statusCode ) ) ;
103
+ // (keep all the error handling in one place)
104
+ throw new HttpSysException ( ( int ) statusCode ) ;
89
105
}
90
106
}
91
107
}
92
108
catch ( Exception exception )
93
109
{
94
- _mrvts . SetException ( exception ) ;
110
+ try
111
+ {
112
+ _mrvts . SetException ( exception ) ;
113
+ }
114
+ catch ( Exception ex )
115
+ {
116
+ Log . AcceptSetResultFailed ( _logger , ex ) ;
117
+ }
95
118
}
96
119
}
97
120
98
121
private static unsafe void IOWaitCallback ( uint errorCode , uint numBytes , NativeOverlapped * nativeOverlapped )
99
122
{
100
123
var acceptContext = ( AsyncAcceptContext ) ThreadPoolBoundHandle . GetNativeOverlappedState ( nativeOverlapped ) ! ;
101
- acceptContext . IOCompleted ( errorCode , numBytes ) ;
124
+ acceptContext . IOCompleted ( errorCode , numBytes , false ) ;
125
+ }
126
+
127
+ private void SetExpectCompletion ( ) // we anticipate a completion *might* occur
128
+ {
129
+ // note this is intentionally a "reset and check" rather than Increment, so that we don't spam
130
+ // the logs forever if a glitch occurs
131
+ var value = Interlocked . Exchange ( ref _expectedCompletionCount , 1 ) ; // should have been 0
132
+ if ( value != 0 )
133
+ {
134
+ if ( _logExpectationFailures )
135
+ {
136
+ Log . AcceptSetExpectationMismatch ( _logger , value ) ;
137
+ }
138
+ Debug . Assert ( false , nameof ( SetExpectCompletion ) ) ; // fail hard in debug
139
+ }
140
+ }
141
+ private void CancelExpectCompletion ( ) // due to error-code etc, we no longer anticipate a completion
142
+ {
143
+ var value = Interlocked . Decrement ( ref _expectedCompletionCount ) ; // should have been 1, so now 0
144
+ if ( value != 0 )
145
+ {
146
+ if ( _logExpectationFailures )
147
+ {
148
+ Log . AcceptCancelExpectationMismatch ( _logger , value ) ;
149
+ }
150
+ Debug . Assert ( false , nameof ( CancelExpectCompletion ) ) ; // fail hard in debug
151
+ }
152
+ }
153
+ private void ObserveCompletion ( bool managed ) // a completion was invoked
154
+ {
155
+ var value = Interlocked . Decrement ( ref _expectedCompletionCount ) ; // should have been 1, so now 0
156
+ if ( value != 0 )
157
+ {
158
+ if ( _logExpectationFailures )
159
+ {
160
+ Log . AcceptObserveExpectationMismatch ( _logger , managed ? "managed" : "unmanaged" , value ) ;
161
+ }
162
+ Debug . Assert ( false , nameof ( ObserveCompletion ) ) ; // fail hard in debug
163
+ }
102
164
}
103
165
104
166
private uint QueueBeginGetContext ( )
@@ -111,6 +173,7 @@ private uint QueueBeginGetContext()
111
173
112
174
retry = false ;
113
175
uint bytesTransferred = 0 ;
176
+ SetExpectCompletion ( ) ; // track this *before*, because of timing vs IOCP (could even be effectively synchronous)
114
177
statusCode = HttpApi . HttpReceiveHttpRequest (
115
178
Server . RequestQueue . Handle ,
116
179
_requestContext . RequestId ,
@@ -122,35 +185,44 @@ private uint QueueBeginGetContext()
122
185
& bytesTransferred ,
123
186
_overlapped ) ;
124
187
125
- if ( ( statusCode == ErrorCodes . ERROR_CONNECTION_INVALID
126
- || statusCode == ErrorCodes . ERROR_INVALID_PARAMETER )
127
- && _requestContext . RequestId != 0 )
128
- {
129
- // ERROR_CONNECTION_INVALID:
130
- // The client reset the connection between the time we got the MORE_DATA error and when we called HttpReceiveHttpRequest
131
- // with the new buffer. We can clear the request id and move on to the next request.
132
- //
133
- // ERROR_INVALID_PARAMETER: Historical check from HttpListener.
134
- // https://referencesource.microsoft.com/#System/net/System/Net/_ListenerAsyncResult.cs,137
135
- // we might get this if somebody stole our RequestId,
136
- // set RequestId to 0 and start all over again with the buffer we just allocated
137
- // BUGBUG: how can someone steal our request ID? seems really bad and in need of fix.
138
- _requestContext . RequestId = 0 ;
139
- retry = true ;
140
- }
141
- else if ( statusCode == ErrorCodes . ERROR_MORE_DATA )
142
- {
143
- // the buffer was not big enough to fit the headers, we need
144
- // to read the RequestId returned, allocate a new buffer of the required size
145
- // (uint)backingBuffer.Length - AlignmentPadding
146
- AllocateNativeRequest ( bytesTransferred ) ;
147
- retry = true ;
148
- }
149
- else if ( statusCode == ErrorCodes . ERROR_SUCCESS
150
- && HttpSysListener . SkipIOCPCallbackOnSuccess )
188
+ switch ( statusCode )
151
189
{
152
- // IO operation completed synchronously - callback won't be called to signal completion.
153
- IOCompleted ( statusCode , bytesTransferred ) ;
190
+ case ( ErrorCodes . ERROR_CONNECTION_INVALID or ErrorCodes . ERROR_INVALID_PARAMETER ) when _requestContext . RequestId != 0 :
191
+ // ERROR_CONNECTION_INVALID:
192
+ // The client reset the connection between the time we got the MORE_DATA error and when we called HttpReceiveHttpRequest
193
+ // with the new buffer. We can clear the request id and move on to the next request.
194
+ //
195
+ // ERROR_INVALID_PARAMETER: Historical check from HttpListener.
196
+ // https://referencesource.microsoft.com/#System/net/System/Net/_ListenerAsyncResult.cs,137
197
+ // we might get this if somebody stole our RequestId,
198
+ // set RequestId to 0 and start all over again with the buffer we just allocated
199
+ // BUGBUG: how can someone steal our request ID? seems really bad and in need of fix.
200
+ CancelExpectCompletion ( ) ;
201
+ _requestContext . RequestId = 0 ;
202
+ retry = true ;
203
+ break ;
204
+ case ErrorCodes . ERROR_MORE_DATA :
205
+ // the buffer was not big enough to fit the headers, we need
206
+ // to read the RequestId returned, allocate a new buffer of the required size
207
+ // (uint)backingBuffer.Length - AlignmentPadding
208
+ CancelExpectCompletion ( ) ; // we'll "expect" again when we retry
209
+ AllocateNativeRequest ( bytesTransferred ) ;
210
+ retry = true ;
211
+ break ;
212
+ case ErrorCodes . ERROR_SUCCESS :
213
+ if ( HttpSysListener . SkipIOCPCallbackOnSuccess )
214
+ {
215
+ // IO operation completed synchronously - callback won't be called to signal completion.
216
+ IOCompleted ( statusCode , bytesTransferred , true ) ; // marks completion
217
+ }
218
+ // else: callback fired by IOCP (at some point), which marks completion
219
+ break ;
220
+ case ErrorCodes . ERROR_IO_PENDING :
221
+ break ; // no change to state - callback will occur at some point
222
+ default :
223
+ // fault code, not expecting an IOCP callback
224
+ CancelExpectCompletion ( ) ;
225
+ break ;
154
226
}
155
227
}
156
228
while ( retry ) ;
0 commit comments