|
| 1 | +using System.Collections; |
| 2 | +using System.Reflection; |
1 | 3 | using Akka.Actor; |
2 | 4 | using GaudiHTTP.Protocol.Body; |
3 | 5 | using GaudiHTTP.Protocol.Syntax.Http2; |
@@ -89,9 +91,6 @@ private static void DrainToCompletion(FlowControlledBodyPump scheduler, FakeTarg |
89 | 91 | case BodyReadComplete<int> rc: |
90 | 92 | scheduler.HandleReadComplete(rc.StreamId, rc.BytesRead); |
91 | 93 | break; |
92 | | - case BodyReadContinue<int> dc: |
93 | | - scheduler.HandleBodyReadContinue(dc.StreamId); |
94 | | - break; |
95 | 94 | } |
96 | 95 | } |
97 | 96 | } |
@@ -369,6 +368,77 @@ public void OrphanRefund_should_return_reserved_window_when_stream_cancelled_dur |
369 | 368 | Assert.Equal(windowBefore, flow.ConnectionSendWindow); |
370 | 369 | } |
371 | 370 |
|
| 371 | + [Fact(Timeout = 5000)] |
| 372 | + public void FailedRead_should_refund_reserved_window() |
| 373 | + { |
| 374 | + var target = new FakeTarget(); |
| 375 | + var flow = MakeFlow(); |
| 376 | + var scheduler = new FlowControlledBodyPump(target, flow, new CancellationTokenSource(), chunkSize: 1 * 1024, hardCap: 16); |
| 377 | + |
| 378 | + flow.InitStreamSendWindow(1); |
| 379 | + var windowBefore = flow.ConnectionSendWindow; |
| 380 | + |
| 381 | + // Async read path so the reservation is held while the read is in flight. |
| 382 | + var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 383 | + var blockingStream = new DelegatingReadStream((_, _) => new ValueTask<int>(tcs.Task)); |
| 384 | + scheduler.Register(1, blockingStream, null, CancellationToken.None); |
| 385 | + |
| 386 | + Assert.Equal(windowBefore - 1 * 1024, flow.ConnectionSendWindow); |
| 387 | + |
| 388 | + // Read fails while the stream is still active (not orphaned): nothing was |
| 389 | + // sent, so the full reservation must be refunded. |
| 390 | + scheduler.HandleReadFailed(1, new IOException("read failed")); |
| 391 | + |
| 392 | + Assert.Single(target.Failed); |
| 393 | + Assert.Equal(windowBefore, flow.ConnectionSendWindow); |
| 394 | + } |
| 395 | + |
| 396 | + [Fact(Timeout = 5000)] |
| 397 | + public void Cancel_should_dispose_queued_non_inflight_slot_immediately_even_when_window_stays_exhausted() |
| 398 | + { |
| 399 | + var target = new FakeTarget(); |
| 400 | + var flow = MakeFlow(connWindow: 65535); |
| 401 | + // hardCap = 1: only one async read may be in flight at a time. |
| 402 | + var scheduler = new FlowControlledBodyPump(target, flow, new CancellationTokenSource(), chunkSize: 64, hardCap: 1); |
| 403 | + |
| 404 | + flow.InitStreamSendWindow(1); |
| 405 | + flow.InitStreamSendWindow(3); |
| 406 | + |
| 407 | + // Stream 1 occupies the only read slot with a read that never completes. |
| 408 | + var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 409 | + var blockingStream = new DelegatingReadStream((_, _) => new ValueTask<int>(tcs.Task)); |
| 410 | + scheduler.Register(1, blockingStream, null, CancellationToken.None); |
| 411 | + |
| 412 | + // Stream 3 has an open window, so it is enqueued — but _asyncInFlight (1) already meets |
| 413 | + // hardCap (1), so it never starts a read: it sits in the ready queue, not in-flight and |
| 414 | + // not window-blocked. |
| 415 | + scheduler.Register(3, MakeBody(100), 100, CancellationToken.None); |
| 416 | + |
| 417 | + var activeSlots = GetActiveSlots(scheduler); |
| 418 | + Assert.True(activeSlots.Contains(3)); |
| 419 | + |
| 420 | + // Cancel stream 3 while queued. The connection window never opens again afterward, |
| 421 | + // simulating a stalled connection — a deferred-dispose discipline would leave the |
| 422 | + // cancelled slot (and its pooled buffer) alive until Cleanup(); the correct discipline |
| 423 | + // disposes it immediately, matching MultiplexedBodyPump. |
| 424 | + scheduler.Cancel(3); |
| 425 | + |
| 426 | + Assert.False(activeSlots.Contains(3)); |
| 427 | + |
| 428 | + // Opening the window afterward must not resurrect or emit anything for stream 3. |
| 429 | + flow.OnSendWindowUpdate(0, 65535); |
| 430 | + scheduler.OnWindowUpdate(0); |
| 431 | + |
| 432 | + Assert.DoesNotContain(3, target.Completed); |
| 433 | + Assert.DoesNotContain(target.Emitted, e => e.StreamId == 3); |
| 434 | + } |
| 435 | + |
| 436 | + private static IDictionary GetActiveSlots(FlowControlledBodyPump scheduler) |
| 437 | + { |
| 438 | + var field = typeof(FlowControlledBodyPump).GetField("_activeSlots", BindingFlags.NonPublic | BindingFlags.Instance)!; |
| 439 | + return (IDictionary)field.GetValue(scheduler)!; |
| 440 | + } |
| 441 | + |
372 | 442 | [Fact(Timeout = 5000)] |
373 | 443 | public void WindowBlocked_should_block_when_available_below_half_chunk() |
374 | 444 | { |
|
0 commit comments