@@ -128,7 +128,7 @@ static async Task RunTest()
128
128
TimeSpan . FromMilliseconds ( 30 ) ,
129
129
TimeSpan . Zero ,
130
130
2 * 1024 * 1024 ,
131
- null ) ;
131
+ maxWindowForPingStopValidation : MaxWindow ) ;
132
132
133
133
Assert . True ( maxCredit <= MaxWindow ) ;
134
134
}
@@ -181,19 +181,34 @@ static async Task RunTest()
181
181
RemoteExecutor . Invoke ( RunTest , options ) . Dispose ( ) ;
182
182
}
183
183
184
+ [ OuterLoop ( "Runs long" ) ]
185
+ [ Fact ]
186
+ public async Task LongRunningSlowServerStream_NoInvalidPingsAreSent ( )
187
+ {
188
+ // A scenario similar to https://github.com/grpc/grpc-dotnet/issues/2361.
189
+ // We need to send a small amount of data so the connection window is not consumed and no "standard" WINDOW_UPDATEs are sent and
190
+ // we also need to do it very slowly to cover some RTT PINGs after the initial burst.
191
+ // This scenario should trigger the "forced WINDOW_UPDATE" logic in the implementation, ensuring that no more than 4 PINGs are sent without a WINDOW_UPDATE.
192
+ await TestClientWindowScalingAsync (
193
+ TimeSpan . FromMilliseconds ( 500 ) ,
194
+ TimeSpan . FromMilliseconds ( 500 ) ,
195
+ 1024 ,
196
+ _output ,
197
+ dataPerFrame : 32 ) ;
198
+ }
199
+
184
200
private static async Task < int > TestClientWindowScalingAsync (
185
201
TimeSpan networkDelay ,
186
202
TimeSpan slowBandwidthSimDelay ,
187
203
int bytesToDownload ,
188
204
ITestOutputHelper output = null ,
189
- int maxWindowForPingStopValidation = int . MaxValue , // set to actual maximum to test if we stop sending PING when window reached maximum
190
- Action < SocketsHttpHandler > configureHandler = null )
205
+ int dataPerFrame = 16384 ,
206
+ int maxWindowForPingStopValidation = 16 * 1024 * 1024 ) // set to actual maximum to test if we stop sending PING when window reached maximum
191
207
{
192
208
TimeSpan timeout = TimeSpan . FromSeconds ( 30 ) ;
193
209
CancellationTokenSource timeoutCts = new CancellationTokenSource ( timeout ) ;
194
210
195
211
HttpClientHandler handler = CreateHttpClientHandler ( HttpVersion20 . Value ) ;
196
- configureHandler ? . Invoke ( GetUnderlyingSocketsHttpHandler ( handler ) ) ;
197
212
198
213
using Http2LoopbackServer server = Http2LoopbackServer . CreateServer ( NoAutoPingResponseHttp2Options ) ;
199
214
using HttpClient client = new HttpClient ( handler , true ) ;
@@ -225,13 +240,13 @@ private static async Task<int> TestClientWindowScalingAsync(
225
240
using SemaphoreSlim writeSemaphore = new SemaphoreSlim ( 1 ) ;
226
241
int remainingBytes = bytesToDownload ;
227
242
228
- bool pingReceivedAfterReachingMaxWindow = false ;
243
+ string unexpectedPingReason = null ;
229
244
bool unexpectedFrameReceived = false ;
230
245
CancellationTokenSource stopFrameProcessingCts = new CancellationTokenSource ( ) ;
231
246
232
247
CancellationTokenSource linkedCts = CancellationTokenSource . CreateLinkedTokenSource ( stopFrameProcessingCts . Token , timeoutCts . Token ) ;
233
248
Task processFramesTask = ProcessIncomingFramesAsync ( linkedCts . Token ) ;
234
- byte [ ] buffer = new byte [ 16384 ] ;
249
+ byte [ ] buffer = new byte [ dataPerFrame ] ;
235
250
236
251
while ( remainingBytes > 0 )
237
252
{
@@ -259,7 +274,7 @@ private static async Task<int> TestClientWindowScalingAsync(
259
274
260
275
int dataReceived = ( await response . Content . ReadAsByteArrayAsync ( ) ) . Length ;
261
276
Assert . Equal ( bytesToDownload , dataReceived ) ;
262
- Assert . False ( pingReceivedAfterReachingMaxWindow , "Server received a PING after reaching max window" ) ;
277
+ Assert . Null ( unexpectedPingReason ) ;
263
278
Assert . False ( unexpectedFrameReceived , "Server received an unexpected frame, see test output for more details." ) ;
264
279
265
280
return maxCredit ;
@@ -270,6 +285,7 @@ async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
270
285
// We should not receive any more RTT PING's after this point
271
286
int maxWindowCreditThreshold = ( int ) ( 0.9 * maxWindowForPingStopValidation ) ;
272
287
output ? . WriteLine ( $ "maxWindowCreditThreshold: { maxWindowCreditThreshold } maxWindowForPingStopValidation: { maxWindowForPingStopValidation } ") ;
288
+ int pingsWithoutWindowUpdate = 0 ;
273
289
274
290
try
275
291
{
@@ -284,10 +300,18 @@ async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
284
300
285
301
output ? . WriteLine ( $ "Received PING ({ pingFrame . Data } )") ;
286
302
303
+ pingsWithoutWindowUpdate ++ ;
287
304
if ( maxCredit > maxWindowCreditThreshold )
288
305
{
289
- output ? . WriteLine ( "PING was unexpected" ) ;
290
- Volatile . Write ( ref pingReceivedAfterReachingMaxWindow , true ) ;
306
+ Volatile . Write ( ref unexpectedPingReason , "The server received a PING after reaching max window" ) ;
307
+ output ? . WriteLine ( $ "PING was unexpected: { unexpectedPingReason } ") ;
308
+ }
309
+
310
+ // Exceeding this limit may trigger a GOAWAY on some servers. See implementation comments for more details.
311
+ if ( pingsWithoutWindowUpdate > 4 )
312
+ {
313
+ Volatile . Write ( ref unexpectedPingReason , $ "The server received { pingsWithoutWindowUpdate } PINGs without receiving a WINDOW_UPDATE") ;
314
+ output ? . WriteLine ( $ "PING was unexpected: { unexpectedPingReason } ") ;
291
315
}
292
316
293
317
await writeSemaphore . WaitAsync ( cancellationToken ) ;
@@ -296,6 +320,7 @@ async Task ProcessIncomingFramesAsync(CancellationToken cancellationToken)
296
320
}
297
321
else if ( frame is WindowUpdateFrame windowUpdateFrame )
298
322
{
323
+ pingsWithoutWindowUpdate = 0 ;
299
324
// Ignore connection window:
300
325
if ( windowUpdateFrame . StreamId != streamId ) continue ;
301
326
0 commit comments