Skip to content

Commit 91ef938

Browse files
authored
Ensure correct ordering of promise notification when connect is still pending. (#330)
Motivation: When a pending connect is still in process and the Channel is closed we need to ensure we use the correct ordering when notify the promises and ChannelHandlers in the pipeline. The ordering should be: - Notify pending connect promise - Notify close promise - Call channelInactive(...) Modifications: - Correct ordering of notification - Add unit test Result: Ensure correct ordering when connect is still pending
1 parent 338cfb5 commit 91ef938

File tree

3 files changed

+102
-5
lines changed

3 files changed

+102
-5
lines changed

Sources/NIO/BaseSocketChannel.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,11 @@ class BaseSocketChannel<T: BaseSocket>: SelectableChannel, ChannelCore {
677677
// Transition our internal state.
678678
let callouts = self.lifecycleManager.close(promise: p)
679679

680+
if let connectPromise = self.pendingConnect {
681+
self.pendingConnect = nil
682+
connectPromise.fail(error: error)
683+
}
684+
680685
// Now that our state is sensible, we can call out to user code.
681686
self.cancelWritesOnClose(error: error)
682687
callouts(self.pipeline)
@@ -687,11 +692,6 @@ class BaseSocketChannel<T: BaseSocket>: SelectableChannel, ChannelCore {
687692

688693
self.closePromise.succeed(result: ())
689694
}
690-
691-
if let connectPromise = pendingConnect {
692-
pendingConnect = nil
693-
connectPromise.fail(error: error)
694-
}
695695
}
696696

697697

Tests/NIOTests/SocketChannelTest+XCTest.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ extension SocketChannelTest {
4242
("testCloseDuringWriteFailure", testCloseDuringWriteFailure),
4343
("testWithConfiguredStreamSocket", testWithConfiguredStreamSocket),
4444
("testWithConfiguredDatagramSocket", testWithConfiguredDatagramSocket),
45+
("testPendingConnectNotificationOrder", testPendingConnectNotificationOrder),
4546
]
4647
}
4748
}

Tests/NIOTests/SocketChannelTest.swift

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,4 +350,100 @@ public class SocketChannelTest : XCTestCase {
350350

351351
try serverChannel.close().wait()
352352
}
353+
354+
public func testPendingConnectNotificationOrder() throws {
355+
356+
class NotificationOrderHandler: ChannelDuplexHandler {
357+
typealias InboundIn = Never
358+
typealias OutboundIn = Never
359+
360+
private var connectPromise: EventLoopPromise<Void>?
361+
362+
public func channelInactive(ctx: ChannelHandlerContext) {
363+
if let connectPromise = self.connectPromise {
364+
XCTAssertTrue(connectPromise.futureResult.isFulfilled)
365+
} else {
366+
XCTFail("connect(...) not called before")
367+
}
368+
}
369+
370+
public func connect(ctx: ChannelHandlerContext, to address: SocketAddress, promise: EventLoopPromise<Void>?) {
371+
XCTAssertNil(self.connectPromise)
372+
self.connectPromise = promise
373+
ctx.connect(to: address, promise: promise)
374+
}
375+
376+
func handlerAdded(ctx: ChannelHandlerContext) {
377+
XCTAssertNil(self.connectPromise)
378+
}
379+
380+
func handlerRemoved(ctx: ChannelHandlerContext) {
381+
if let connectPromise = self.connectPromise {
382+
XCTAssertTrue(connectPromise.futureResult.isFulfilled)
383+
} else {
384+
XCTFail("connect(...) not called before")
385+
}
386+
}
387+
}
388+
389+
let group = MultiThreadedEventLoopGroup(numThreads: 1)
390+
defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) }
391+
392+
let serverChannel = try ServerBootstrap(group: group).bind(host: "127.0.0.1", port: 0).wait()
393+
defer { XCTAssertNoThrow(try serverChannel.close().wait()) }
394+
395+
let eventLoop = group.next()
396+
let promise: EventLoopPromise<Void> = eventLoop.newPromise()
397+
398+
class ConnectPendingSocket: Socket {
399+
let promise: EventLoopPromise<Void>
400+
init(promise: EventLoopPromise<Void>) throws {
401+
self.promise = promise
402+
try super.init(protocolFamily: PF_INET, type: Posix.SOCK_STREAM)
403+
}
404+
405+
override func connect(to address: SocketAddress) throws -> Bool {
406+
// We want to return false here to have a pending connect.
407+
_ = try super.connect(to: address)
408+
self.promise.succeed(result: ())
409+
return false
410+
}
411+
}
412+
413+
let channel = try SocketChannel(socket: ConnectPendingSocket(promise: promise), parent: nil, eventLoop: eventLoop as! SelectableEventLoop)
414+
let connectPromise: EventLoopPromise<Void> = channel.eventLoop.newPromise()
415+
let closePromise: EventLoopPromise<Void> = channel.eventLoop.newPromise()
416+
417+
closePromise.futureResult.whenComplete {
418+
XCTAssertTrue(connectPromise.futureResult.isFulfilled)
419+
}
420+
connectPromise.futureResult.whenComplete {
421+
XCTAssertFalse(closePromise.futureResult.isFulfilled)
422+
}
423+
424+
XCTAssertNoThrow(try channel.pipeline.add(handler: NotificationOrderHandler()).wait())
425+
426+
// We need to call submit {...} here to ensure then {...} is called while on the EventLoop already to not have
427+
// a ECONNRESET sneak in.
428+
XCTAssertNoThrow(try channel.eventLoop.submit {
429+
channel.register().map { () -> Void in
430+
channel.connect(to: serverChannel.localAddress!, promise: connectPromise)
431+
}.map { () -> Void in
432+
XCTAssertFalse(connectPromise.futureResult.isFulfilled)
433+
// The close needs to happen in the then { ... } block to ensure we close the channel
434+
// before we have the chance to register it for .write.
435+
channel.close(promise: closePromise)
436+
}
437+
}.wait().wait() as Void)
438+
439+
do {
440+
try connectPromise.futureResult.wait()
441+
XCTFail("Did not throw")
442+
} catch let err as ChannelError where err == .alreadyClosed {
443+
// expected
444+
}
445+
XCTAssertNoThrow(try closePromise.futureResult.wait())
446+
XCTAssertNoThrow(try channel.closeFuture.wait())
447+
XCTAssertNoThrow(try promise.futureResult.wait())
448+
}
353449
}

0 commit comments

Comments
 (0)