diff --git a/Sources/NIOPosix/HappyEyeballs.swift b/Sources/NIOPosix/HappyEyeballs.swift index 015a8b4af6..25071ec32a 100644 --- a/Sources/NIOPosix/HappyEyeballs.swift +++ b/Sources/NIOPosix/HappyEyeballs.swift @@ -464,7 +464,12 @@ internal final class HappyEyeballsConnector { // notifications, and can also get late scheduled task callbacks. We want to just quietly // ignore these, as our transition into the complete state should have already sent // cleanup messages to all of these things. - case (.complete, .resolverACompleted), + // + // We can also get the resolutionDelayElapsed after allResolved, as it's possible that + // callback was already dequeued in the same tick as the cancellation. That's also fine: + // the resolution delay isn't interesting. + case (.allResolved, .resolutionDelayElapsed), + (.complete, .resolverACompleted), (.complete, .resolverAAAACompleted), (.complete, .connectSuccess), (.complete, .connectFailed), diff --git a/Tests/NIOPosixTests/HappyEyeballsTest.swift b/Tests/NIOPosixTests/HappyEyeballsTest.swift index 66f0dec09d..29f35820f8 100644 --- a/Tests/NIOPosixTests/HappyEyeballsTest.swift +++ b/Tests/NIOPosixTests/HappyEyeballsTest.swift @@ -1308,4 +1308,51 @@ public final class HappyEyeballsTest: XCTestCase { XCTAssertNoThrow(try client.close().wait()) } + + func testResolutionTimeoutAndResolutionInSameTick() throws { + var channels: [Channel] = [] + let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80) { + let channelFuture = defaultChannelBuilder(loop: $0, family: $1) + channelFuture.whenSuccess { channel in + try! channel.pipeline.addHandler(ConnectionDelayer(), name: CONNECT_DELAYER, position: .first).wait() + channels.append(channel) + } + return channelFuture + } + let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in + let target = channel.connectTarget() + _ = try (channel as! EmbeddedChannel).finish() + return target + } + loop.run() + + // Then, queue a task to resolve the v6 promise after 50ms. + // Why 50ms? This is the same time as the resolution delay. + let promise = resolver.v6Promise + loop.scheduleTask(in: .milliseconds(50)) { + promise.fail(DummyError()) + } + + // Kick off the IPv4 resolution. This triggers the timer for the resolution delay. + resolver.v4Promise.succeed(SINGLE_IPv4_RESULT) + loop.run() + + // Advance time 50ms. + loop.advanceTime(by: .milliseconds(50)) + + // Then complete the connection future. + XCTAssertEqual(channels.count, 1) + channels.first!.succeedConnection() + + // Should be done. + let target = try targetFuture.wait() + XCTAssertEqual(target!, "10.0.0.1") + + // We should have had queries for AAAA and A. + let expectedQueries: [DummyResolver.Event] = [ + .aaaa(host: "example.com", port: 80), + .a(host: "example.com", port: 80), + ] + XCTAssertEqual(resolver.events, expectedQueries) + } }