@@ -623,4 +623,104 @@ class HTTPClientInternalTests: XCTestCase {
623
623
}
624
624
XCTAssertNoThrow ( try client. syncShutdown ( ) )
625
625
}
626
+
627
+ func testRaceBetweenAsynchronousCloseAndChannelUsabilityDetection( ) {
628
+ final class DelayChannelCloseUntilToldHandler : ChannelOutboundHandler {
629
+ typealias OutboundIn = Any
630
+
631
+ enum State {
632
+ case idling
633
+ case delayedClose
634
+ case closeDone
635
+ }
636
+
637
+ var state : State = . idling
638
+ let doTheCloseNowFuture : EventLoopFuture < Void >
639
+ let sawTheClosePromise : EventLoopPromise < Void >
640
+
641
+ init ( doTheCloseNowFuture: EventLoopFuture < Void > ,
642
+ sawTheClosePromise: EventLoopPromise < Void > ) {
643
+ self . doTheCloseNowFuture = doTheCloseNowFuture
644
+ self . sawTheClosePromise = sawTheClosePromise
645
+ }
646
+
647
+ func handlerRemoved( context: ChannelHandlerContext ) {
648
+ XCTAssertEqual ( . closeDone, self . state)
649
+ }
650
+
651
+ func close( context: ChannelHandlerContext , mode: CloseMode , promise: EventLoopPromise < Void > ? ) {
652
+ XCTAssertEqual ( . idling, self . state)
653
+ self . state = . delayedClose
654
+ self . sawTheClosePromise. succeed ( ( ) )
655
+ // let's hold the close until the future's complete
656
+ self . doTheCloseNowFuture. whenSuccess {
657
+ context. close ( mode: mode) . map {
658
+ XCTAssertEqual ( . delayedClose, self . state)
659
+ self . state = . closeDone
660
+ } . cascade ( to: promise)
661
+ }
662
+ }
663
+ }
664
+
665
+ let web = HTTPBin ( )
666
+ defer {
667
+ XCTAssertNoThrow ( try web. shutdown ( ) )
668
+ }
669
+
670
+ let client = HTTPClient ( eventLoopGroupProvider: . createNew)
671
+ defer {
672
+ XCTAssertNoThrow ( try client. syncShutdown ( ) )
673
+ }
674
+
675
+ let req = try ! HTTPClient . Request ( url: " http://localhost: \( web. serverChannel. localAddress!. port!) /get " ,
676
+ method: . GET,
677
+ body: nil )
678
+
679
+ // Let's start by getting a connection so we can mess with the Channel :).
680
+ var maybeConnection : ConnectionPool . Connection ?
681
+ XCTAssertNoThrow ( try maybeConnection = client. pool. getConnection ( for: req,
682
+ preference: . indifferent,
683
+ on: client. eventLoopGroup. next ( ) ,
684
+ deadline: nil ) . wait ( ) )
685
+ guard let connection = maybeConnection else {
686
+ XCTFail ( " couldn't make connection " )
687
+ return
688
+ }
689
+
690
+ let channel = connection. channel
691
+ let doActualCloseNowPromise = channel. eventLoop. makePromise ( of: Void . self)
692
+ let sawTheClosePromise = channel. eventLoop. makePromise ( of: Void . self)
693
+
694
+ XCTAssertNoThrow ( try channel. pipeline. addHandler ( DelayChannelCloseUntilToldHandler ( doTheCloseNowFuture: doActualCloseNowPromise. futureResult,
695
+ sawTheClosePromise: sawTheClosePromise) ,
696
+ position: . first) . wait ( ) )
697
+ client. pool. release ( connection)
698
+
699
+ XCTAssertNoThrow ( try client. execute ( request: req) . wait ( ) )
700
+
701
+ // Now, let's pretend the timeout happened
702
+ channel. pipeline. fireUserInboundEventTriggered ( IdleStateHandler . IdleStateEvent. write)
703
+
704
+ // The Channel's closure should have already been initialised now but still, let's make sure the close
705
+ // was initiated
706
+ XCTAssertNoThrow ( try sawTheClosePromise. futureResult. wait ( ) )
707
+ // The Channel should still be active though because we delayed the close through our handler above.
708
+ XCTAssertTrue ( channel. isActive)
709
+
710
+ // When asking for a connection again, we should _not_ get the same one back because we did most of the close,
711
+ // similar to what the SSLHandler would do.
712
+ XCTAssertNoThrow ( try maybeConnection = client. pool. getConnection ( for: req,
713
+ preference: . indifferent,
714
+ on: client. eventLoopGroup. next ( ) ,
715
+ deadline: nil ) . wait ( ) )
716
+ doActualCloseNowPromise. succeed ( ( ) )
717
+ guard let connection2 = maybeConnection else {
718
+ XCTFail ( " couldn't get second connection " )
719
+ return
720
+ }
721
+
722
+ XCTAssert ( connection !== connection2)
723
+ client. pool. release ( connection2)
724
+ XCTAssertTrue ( connection2. channel. isActive)
725
+ }
626
726
}
0 commit comments