swift-nio/Tests/NIOPosixTests/HappyEyeballsTest.swift

1235 lines
48 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
import Darwin
#else
import Glibc
#endif
import XCTest
@testable import NIOCore
import NIOEmbedded
@testable import NIOPosix
private let CONNECT_RECORDER = "connectRecorder"
private let CONNECT_DELAYER = "connectDelayer"
private let SINGLE_IPv6_RESULT = [SocketAddress(host: "example.com", ipAddress: "fe80::1", port: 80)]
private let SINGLE_IPv4_RESULT = [SocketAddress(host: "example.com", ipAddress: "10.0.0.1", port: 80)]
private let MANY_IPv6_RESULTS = (1...10).map { SocketAddress(host: "example.com", ipAddress: "fe80::\($0)", port: 80) }
private let MANY_IPv4_RESULTS = (1...10).map { SocketAddress(host: "example.com", ipAddress: "10.0.0.\($0)", port: 80) }
private extension Array where Element == Channel {
func finishAll() {
self.forEach {
do {
_ = try($0 as! EmbeddedChannel).finish()
// We're happy with no error
} catch ChannelError.alreadyClosed {
return // as well as already closed.
} catch {
XCTFail("Finishing got error \(error)")
}
}
}
}
private class DummyError: Error, Equatable {
// For dummy error equality is identity.
static func ==(lhs: DummyError, rhs: DummyError) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}
private class ConnectRecorder: ChannelOutboundHandler {
typealias OutboundIn = Any
typealias OutboundOut = Any
enum State {
case idle
case connected
case closed
}
var targetHost: String?
var state: State = .idle
public func connect(context: ChannelHandlerContext, to: SocketAddress, promise: EventLoopPromise<Void>?) {
self.targetHost = to.toString()
let connectPromise = promise ?? context.eventLoop.makePromise()
connectPromise.futureResult.whenSuccess {
self.state = .connected
}
context.connect(to: to, promise: connectPromise)
}
public func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) {
let connectPromise = promise ?? context.eventLoop.makePromise()
connectPromise.futureResult.whenComplete { (_: Result<Void, Error>) in
self.state = .closed
}
context.close(promise: connectPromise)
}
}
private class ConnectionDelayer: ChannelOutboundHandler {
typealias OutboundIn = Any
typealias OutboundOut = Any
public var connectPromise: EventLoopPromise<Void>?
public func connect(context: ChannelHandlerContext, to address: SocketAddress, promise: EventLoopPromise<Void>?) {
self.connectPromise = promise
}
}
private extension Channel {
func connectTarget() -> String? {
return try! self.pipeline.context(name: CONNECT_RECORDER).map {
($0.handler as! ConnectRecorder).targetHost
}.wait()
}
func succeedConnection() {
return try! self.pipeline.context(name: CONNECT_DELAYER).map {
($0.handler as! ConnectionDelayer).connectPromise!.succeed(())
}.wait()
}
func failConnection(error: Error) {
return try! self.pipeline.context(name: CONNECT_DELAYER).map {
($0.handler as! ConnectionDelayer).connectPromise!.fail(error)
}.wait()
}
func state() -> ConnectRecorder.State {
return try! self.pipeline.context(name: CONNECT_RECORDER).map {
($0.handler as! ConnectRecorder).state
}.flatMapErrorThrowing {
switch $0 {
case ChannelPipelineError.notFound:
return .closed
default:
throw $0
}
}.wait()
}
}
private extension SocketAddress {
init(host: String, ipAddress: String, port: Int) {
do {
var v4addr = in_addr()
try NIOBSDSocket.inet_pton(addressFamily: .inet, addressDescription: ipAddress, address: &v4addr)
var sockaddr = sockaddr_in()
sockaddr.sin_family = sa_family_t(NIOBSDSocket.AddressFamily.inet.rawValue)
sockaddr.sin_port = in_port_t(port).bigEndian
sockaddr.sin_addr = v4addr
self = .init(sockaddr, host: host)
} catch {
do {
var v6addr = in6_addr()
try NIOBSDSocket.inet_pton(addressFamily: .inet6, addressDescription: ipAddress, address: &v6addr)
var sockaddr = sockaddr_in6()
sockaddr.sin6_family = sa_family_t(NIOBSDSocket.AddressFamily.inet6.rawValue)
sockaddr.sin6_port = in_port_t(port).bigEndian
sockaddr.sin6_flowinfo = 0
sockaddr.sin6_scope_id = 0
sockaddr.sin6_addr = v6addr
self = .init(sockaddr, host: host)
} catch {
fatalError("Unable to convert to IP")
}
}
}
func toString() -> String {
let ptr = UnsafeMutableRawPointer.allocate(byteCount: 256, alignment: 1).bindMemory(to: Int8.self, capacity: 256)
switch self {
case .v4(let address):
var baseAddress = address.address
try! NIOBSDSocket.inet_ntop(addressFamily: .inet, addressBytes: &baseAddress.sin_addr, addressDescription: ptr, addressDescriptionLength: 256)
case .v6(let address):
var baseAddress = address.address
try! NIOBSDSocket.inet_ntop(addressFamily: .inet6, addressBytes: &baseAddress.sin6_addr, addressDescription: ptr, addressDescriptionLength: 256)
case .unixDomainSocket:
fatalError("No UDS support in happy eyeballs.")
}
let ipString = String(cString: ptr)
ptr.deinitialize(count: 256).deallocate()
return ipString
}
}
private extension EventLoopFuture {
func getError() -> Error? {
guard self.isFulfilled else { return nil }
var error: Error? = nil
self.whenFailure { error = $0 }
return error!
}
}
// A simple resolver that allows control over the DNS resolution process.
private class DummyResolver: Resolver {
let v4Promise: EventLoopPromise<[SocketAddress]>
let v6Promise: EventLoopPromise<[SocketAddress]>
enum Event {
case a(host: String, port: Int)
case aaaa(host: String, port: Int)
case cancel
}
var events: [Event] = []
init(loop: EventLoop) {
self.v4Promise = loop.makePromise()
self.v6Promise = loop.makePromise()
}
func initiateAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> {
events.append(.a(host: host, port: port))
return self.v4Promise.futureResult
}
func initiateAAAAQuery(host: String, port: Int) -> EventLoopFuture<[SocketAddress]> {
events.append(.aaaa(host: host, port: port))
return self.v6Promise.futureResult
}
func cancelQueries() {
events.append(.cancel)
}
}
extension DummyResolver.Event: Equatable {
}
private func defaultChannelBuilder(loop: EventLoop, family: NIOBSDSocket.ProtocolFamily) -> EventLoopFuture<Channel> {
let channel = EmbeddedChannel(loop: loop as! EmbeddedEventLoop)
XCTAssertNoThrow(try channel.pipeline.addHandler(ConnectRecorder(), name: CONNECT_RECORDER).wait())
return loop.makeSucceededFuture(channel)
}
private func buildEyeballer(host: String,
port: Int,
connectTimeout: TimeAmount = .seconds(10),
channelBuilderCallback: @escaping (EventLoop, NIOBSDSocket.ProtocolFamily) -> EventLoopFuture<Channel> = defaultChannelBuilder) -> (eyeballer: HappyEyeballsConnector, resolver: DummyResolver, loop: EmbeddedEventLoop) {
let loop = EmbeddedEventLoop()
let resolver = DummyResolver(loop: loop)
let eyeballer = HappyEyeballsConnector(resolver: resolver,
loop: loop,
host: host,
port: port,
connectTimeout: connectTimeout,
channelBuilderCallback: channelBuilderCallback)
return (eyeballer: eyeballer, resolver: resolver, loop: loop)
}
public final class HappyEyeballsTest : XCTestCase {
func testIPv4OnlyResolution() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
let target = channel.connectTarget()
_ = try (channel as! EmbeddedChannel).finish()
return target
}
loop.run()
resolver.v6Promise.fail(DummyError())
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.run()
// No time should have needed to pass: we return only one target and it connects immediately.
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)
}
func testIPv6OnlyResolution() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
let target = channel.connectTarget()
_ = try (channel as! EmbeddedChannel).finish()
return target
}
loop.run()
resolver.v4Promise.fail(DummyError())
resolver.v6Promise.succeed(SINGLE_IPv6_RESULT)
loop.run()
// No time should have needed to pass: we return only one target and it connects immediately.
let target = try targetFuture.wait()
XCTAssertEqual(target!, "fe80::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)
}
func testTimeOutDuringDNSResolution() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .seconds(10))
let channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(resolver.events, expectedQueries)
loop.advanceTime(by: .seconds(9))
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(resolver.events, expectedQueries)
loop.advanceTime(by: .seconds(1))
XCTAssertEqual(resolver.events, expectedQueries + [.cancel])
if case .some(ChannelError.connectTimeout(let amount)) = channelFuture.getError() {
XCTAssertEqual(amount, .seconds(10))
} else {
XCTFail("Got \(String(describing: channelFuture.getError()))")
}
// We now want to confirm that nothing awful happens if those DNS results
// return late.
resolver.v6Promise.succeed(SINGLE_IPv6_RESULT)
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.run()
XCTAssertEqual(resolver.events, expectedQueries + [.cancel])
}
func testAAAAQueryReturningFirst() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
let target = channel.connectTarget()
_ = try (channel as! EmbeddedChannel).finish()
return target
}
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
resolver.v6Promise.succeed(SINGLE_IPv6_RESULT)
loop.run()
// No time should have needed to pass: we return only one target and it connects immediately.
let target = try targetFuture.wait()
XCTAssertEqual(target!, "fe80::1")
// We should have had queries for AAAA and A. We should then have had a cancel, because the A
// never returned.
XCTAssertEqual(resolver.events, expectedQueries + [.cancel])
// Now return a result for the IPv4 query. Nothing bad should happen.
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.run()
XCTAssertEqual(resolver.events, expectedQueries + [.cancel])
}
func testAQueryReturningFirstDelayElapses() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
let target = channel.connectTarget()
_ = try (channel as! EmbeddedChannel).finish()
return target
}
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.run()
// There should have been no connection attempt yet.
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
// Let the resolution delay (default of 50 ms) elapse.
loop.advanceTime(by: .milliseconds(49))
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
loop.advanceTime(by: .milliseconds(1))
// The connection attempt should have been made with the IPv4 result.
let target = try targetFuture.wait()
XCTAssertEqual(target!, "10.0.0.1")
// We should have had queries for AAAA and A. We should then have had a cancel, because the A
// never returned.
XCTAssertEqual(resolver.events, expectedQueries + [.cancel])
// Now return a result for the IPv6 query. Nothing bad should happen.
resolver.v6Promise.succeed(SINGLE_IPv6_RESULT)
loop.run()
XCTAssertEqual(resolver.events, expectedQueries + [.cancel])
}
func testAQueryReturningFirstThenAAAAReturns() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
let target = channel.connectTarget()
_ = try (channel as! EmbeddedChannel).finish()
return target
}
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.run()
// There should have been no connection attempt yet.
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
// Now the AAAA returns.
resolver.v6Promise.succeed(SINGLE_IPv6_RESULT)
loop.run()
// The connection attempt should have been made with the IPv6 result.
let target = try targetFuture.wait()
XCTAssertEqual(target!, "fe80::1")
// We should have had queries for AAAA and A, with no cancel.
XCTAssertEqual(resolver.events, expectedQueries)
}
func testAQueryReturningFirstThenAAAAErrors() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
let target = channel.connectTarget()
_ = try (channel as! EmbeddedChannel).finish()
return target
}
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.run()
// There should have been no connection attempt yet.
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
// Now the AAAA fails.
resolver.v6Promise.fail(DummyError())
loop.run()
// The connection attempt should have been made with the IPv4 result.
let target = try targetFuture.wait()
XCTAssertEqual(target!, "10.0.0.1")
// We should have had queries for AAAA and A, with no cancel.
XCTAssertEqual(resolver.events, expectedQueries)
}
func testAQueryReturningFirstThenEmptyAAAA() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let targetFuture = eyeballer.resolveAndConnect().flatMapThrowing { (channel) -> String? in
let target = channel.connectTarget()
_ = try (channel as! EmbeddedChannel).finish()
return target
}
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.run()
// There should have been no connection attempt yet.
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(targetFuture.isFulfilled)
// Now the AAAA returns empty.
resolver.v6Promise.succeed([])
loop.run()
// The connection attempt should have been made with the IPv4 result.
let target = try targetFuture.wait()
XCTAssertEqual(target!, "10.0.0.1")
// We should have had queries for AAAA and A, with no cancel.
XCTAssertEqual(resolver.events, expectedQueries)
}
func testEmptyResultsFail() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
resolver.v4Promise.succeed([])
resolver.v6Promise.succeed([])
loop.run()
// We should have had queries for AAAA and A, with no cancel.
XCTAssertEqual(resolver.events, expectedQueries)
// But we should have failed.
if let error = channelFuture.getError() as? NIOConnectionError {
XCTAssertEqual(error.host, "example.com")
XCTAssertEqual(error.port, 80)
XCTAssertNil(error.dnsAError)
XCTAssertNil(error.dnsAAAAError)
XCTAssertEqual(error.connectionErrors.count, 0)
} else {
XCTFail("Got \(String(describing: channelFuture.getError()))")
}
}
func testAllDNSFail() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80)
let channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
let v4Error = DummyError()
let v6Error = DummyError()
resolver.v4Promise.fail(v4Error)
resolver.v6Promise.fail(v6Error)
loop.run()
// We should have had queries for AAAA and A, with no cancel.
XCTAssertEqual(resolver.events, expectedQueries)
// But we should have failed.
if let error = channelFuture.getError() as? NIOConnectionError {
XCTAssertEqual(error.host, "example.com")
XCTAssertEqual(error.port, 80)
XCTAssertEqual(error.dnsAError as? DummyError ?? DummyError(), v4Error)
XCTAssertEqual(error.dnsAAAAError as? DummyError ?? DummyError(), v6Error)
XCTAssertEqual(error.connectionErrors.count, 0)
} else {
XCTFail("Got \(String(describing: channelFuture.getError()))")
}
}
func testMaximalConnectionDelay() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .hours(1)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// We're providing the IPv4 and IPv6 results. This will lead to 20 total hosts
// for us to try to connect to.
resolver.v4Promise.succeed(MANY_IPv4_RESULTS)
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
for connectionCount in 1...20 {
XCTAssertEqual(channels.count, connectionCount)
loop.advanceTime(by: .milliseconds(249))
XCTAssertEqual(channels.count, connectionCount)
loop.advanceTime(by: .milliseconds(1))
}
// Now there will be no further connection attempts, even if we advance by a few minutes.
loop.advanceTime(by: .minutes(5))
XCTAssertEqual(channels.count, 20)
// Check that we attempted to connect to these hosts in the appropriate interleaved
// order.
var expectedAddresses = [String]()
for endIndex in 1...10 {
expectedAddresses.append("fe80::\(endIndex)")
expectedAddresses.append("10.0.0.\(endIndex)")
}
let actualAddresses = channels.map { $0.connectTarget()! }
XCTAssertEqual(actualAddresses, expectedAddresses)
// We still shouldn't have actually connected.
XCTAssertFalse(channelFuture.isFulfilled)
for channel in channels {
XCTAssertEqual(channel.state(), .idle)
}
// Connect the last channel. This should immediately succeed the
// future.
channels.last!.succeedConnection()
XCTAssertTrue(channelFuture.isFulfilled)
let connectedChannel = try! channelFuture.wait()
XCTAssertTrue(connectedChannel === channels.last)
XCTAssertEqual(connectedChannel.state(), .connected)
// The other channels should be closed.
for channel in channels.dropLast() {
XCTAssertEqual(channel.state(), .closed)
}
}
func testAllConnectionsFail() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .hours(1)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// We're providing the IPv4 and IPv6 results. This will lead to 20 total hosts
// for us to try to connect to.
resolver.v4Promise.succeed(MANY_IPv4_RESULTS)
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
// Let all the connections fire.
for _ in 1...20 {
loop.advanceTime(by: .milliseconds(250))
}
// We still shouldn't have actually connected.
XCTAssertEqual(channels.count, 20)
XCTAssertFalse(channelFuture.isFulfilled)
for channel in channels {
XCTAssertEqual(channel.state(), .idle)
}
// Fail all the connection attempts.
var errors = [DummyError]()
for channel in channels.dropLast() {
let error = DummyError()
errors.append(error)
channel.failConnection(error: error)
XCTAssertFalse(channelFuture.isFulfilled)
}
// Fail the last channel. This should immediately fail the future.
errors.append(DummyError())
channels.last!.failConnection(error: errors.last!)
XCTAssertTrue(channelFuture.isFulfilled)
// Check the error.
if let error = channelFuture.getError() as? NIOConnectionError {
XCTAssertEqual(error.host, "example.com")
XCTAssertEqual(error.port, 80)
XCTAssertNil(error.dnsAError)
XCTAssertNil(error.dnsAAAAError)
XCTAssertEqual(error.connectionErrors.count, 20)
for (idx, error) in error.connectionErrors.enumerated() {
XCTAssertEqual(error.error as? DummyError, errors[idx])
}
} else {
XCTFail("Got \(String(describing: channelFuture.getError()))")
}
}
func testDelayedAAAAResult() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .hours(1)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Provide the IPv4 results and let five connection attempts play out.
resolver.v4Promise.succeed(MANY_IPv4_RESULTS)
loop.advanceTime(by: .milliseconds(50))
for connectionCount in 1...4 {
XCTAssertEqual(channels.last!.connectTarget()!, "10.0.0.\(connectionCount)")
loop.advanceTime(by: .milliseconds(250))
}
XCTAssertEqual(channels.last!.connectTarget()!, "10.0.0.5")
// Now the IPv6 results come in.
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
// The next 10 connection attempts will interleave the IPv6 and IPv4 results,
// starting with IPv6.
for connectionCount in 1...5 {
loop.advanceTime(by: .milliseconds(250))
XCTAssertEqual(channels.last!.connectTarget()!, "fe80::\(connectionCount)")
loop.advanceTime(by: .milliseconds(250))
XCTAssertEqual(channels.last!.connectTarget()!, "10.0.0.\(connectionCount + 5)")
}
// We're now out of IPv4 addresses, so the last 5 will be IPv6.
for connectionCount in 6...10 {
loop.advanceTime(by: .milliseconds(250))
XCTAssertEqual(channels.last!.connectTarget()!, "fe80::\(connectionCount)")
}
}
func testTimeoutWaitingForAAAA() throws {
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .milliseconds(49))
let channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the A result returns, but the timeout is sufficiently low that the connect attempt
// times out before the AAAA can return.
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.advanceTime(by: .milliseconds(48))
XCTAssertFalse(channelFuture.isFulfilled)
loop.advanceTime(by: .milliseconds(1))
XCTAssertTrue(channelFuture.isFulfilled)
// We should have had queries for AAAA and A. We should then have had a cancel, because the AAAA
// never returned.
XCTAssertEqual(resolver.events, expectedQueries + [.cancel])
switch channelFuture.getError() {
case .some(ChannelError.connectTimeout(.milliseconds(49))):
break
default:
XCTFail("Got unexpected error: \(String(describing: channelFuture.getError()))")
}
}
func testTimeoutAfterAQuery() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .milliseconds(100)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the A result returns, but the timeout is sufficiently low that the connect attempt
// times out before the AAAA can return and before the connection succeeds.
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.advanceTime(by: .milliseconds(99))
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, 1)
XCTAssertEqual(channels.first!.state(), .idle)
// Now the timeout fires.
loop.advanceTime(by: .milliseconds(1))
XCTAssertTrue(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, 1)
XCTAssertEqual(channels.first!.state(), .closed)
switch channelFuture.getError() {
case .some(ChannelError.connectTimeout(.milliseconds(100))):
break
default:
XCTFail("Got unexpected error: \(String(describing: channelFuture.getError()))")
}
}
func testAConnectFailsWaitingForAAAA() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .milliseconds(100)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the A result returns and a connection attempt is made. This fails, and we test that
// we wait for the AAAA query to come in before acting. That connection attempt then times out.
resolver.v4Promise.succeed(SINGLE_IPv4_RESULT)
loop.advanceTime(by: .milliseconds(50))
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, 1)
XCTAssertEqual(channels.first!.state(), .idle)
// The connection attempt fails. We still have no answer.
channels.first!.failConnection(error: DummyError())
XCTAssertFalse(channelFuture.isFulfilled)
// Now the AAAA returns.
resolver.v6Promise.succeed(SINGLE_IPv6_RESULT)
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, 2)
XCTAssertEqual(channels.last!.state(), .idle)
// Now the timeout fires.
loop.advanceTime(by: .milliseconds(50))
XCTAssertTrue(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, 2)
XCTAssertEqual(channels.last!.state(), .closed)
switch channelFuture.getError() {
case .some(ChannelError.connectTimeout(.milliseconds(100))):
break
default:
XCTFail("Got unexpected error: \(String(describing: channelFuture.getError()))")
}
}
func testDelayedAResult() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .hours(1)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Provide the IPv6 results and let all 10 connection attempts play out.
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
for connectionCount in 1...10 {
XCTAssertEqual(channels.last!.connectTarget()!, "fe80::\(connectionCount)")
loop.advanceTime(by: .milliseconds(250))
}
XCTAssertFalse(channelFuture.isFulfilled)
// Advance time by 30 minutes just to prove that we'll wait a long, long time for the
// A result.
loop.advanceTime(by: .minutes(30))
XCTAssertFalse(channelFuture.isFulfilled)
// Now the IPv4 results come in. Let all 10 connection attempts play out.
resolver.v4Promise.succeed(MANY_IPv4_RESULTS)
for connectionCount in 1...10 {
XCTAssertEqual(channels.last!.connectTarget()!, "10.0.0.\(connectionCount)")
loop.advanceTime(by: .milliseconds(250))
}
XCTAssertFalse(channelFuture.isFulfilled)
}
func testTimeoutBeforeAResponse() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .milliseconds(100)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the AAAA result returns, but the timeout is sufficiently low that the connect attempt
// times out before the A returns.
resolver.v6Promise.succeed(SINGLE_IPv6_RESULT)
loop.advanceTime(by: .milliseconds(99))
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, 1)
XCTAssertEqual(channels.first!.state(), .idle)
// Now the timeout fires.
loop.advanceTime(by: .milliseconds(1))
XCTAssertTrue(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, 1)
XCTAssertEqual(channels.first!.state(), .closed)
switch channelFuture.getError() {
case .some(ChannelError.connectTimeout(.milliseconds(100))):
break
default:
XCTFail("Got unexpected error: \(String(describing: channelFuture.getError()))")
}
}
func testAllConnectionsFailImmediately() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the AAAA and A results return. We are going to fail the connections
// instantly, which should cause all 20 to appear.
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
for channelCount in 1...10 {
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, channelCount)
XCTAssertEqual(channels.last!.state(), .idle)
channels.last?.failConnection(error: DummyError())
}
resolver.v4Promise.succeed(MANY_IPv4_RESULTS)
for channelCount in 11...20 {
XCTAssertFalse(channelFuture.isFulfilled)
XCTAssertEqual(channels.count, channelCount)
XCTAssertEqual(channels.last!.state(), .idle)
channels.last?.failConnection(error: DummyError())
}
XCTAssertTrue(channelFuture.isFulfilled)
switch channelFuture.getError() {
case is NIOConnectionError:
break
default:
XCTFail("Got unexpected error: \(String(describing: channelFuture.getError()))")
}
}
func testLaterConnections() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the AAAA results return. Let all the connection attempts go out.
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
for channelCount in 1...10 {
XCTAssertEqual(channels.count, channelCount)
loop.advanceTime(by: .milliseconds(250))
}
// Now we want to connect the first of these. This should lead to success.
channels.first!.succeedConnection()
XCTAssertTrue((try? channelFuture.wait()) === channels.first)
// Now we're going to accept the second connection as well. This should lead to
// a call to close.
channels[1].succeedConnection()
XCTAssertEqual(channels[1].state(), .closed)
XCTAssertTrue((try? channelFuture.wait()) === channels.first)
// Now fail the third. This shouldn't change anything.
channels[2].failConnection(error: DummyError())
XCTAssertTrue((try? channelFuture.wait()) === channels.first)
}
func testDelayedChannelCreation() throws {
var ourChannelFutures: [EventLoopPromise<Channel>] = []
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80) { loop, _ in
ourChannelFutures.append(loop.makePromise())
return ourChannelFutures.last!.futureResult
}
let channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Return the IPv6 results and observe the channel creation attempts.
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
for channelCount in 1...10 {
XCTAssertEqual(ourChannelFutures.count, channelCount)
loop.advanceTime(by: .milliseconds(250))
}
XCTAssertFalse(channelFuture.isFulfilled)
// Succeed the first channel future, which will connect because the default
// channel builder always does.
defaultChannelBuilder(loop: loop, family: .inet6).whenSuccess {
ourChannelFutures.first!.succeed($0)
XCTAssertEqual($0.state(), .connected)
}
XCTAssertTrue(channelFuture.isFulfilled)
// Ok, now succeed the second channel future. This should cause the channel to immediately be closed.
defaultChannelBuilder(loop: loop, family: .inet6).whenSuccess {
ourChannelFutures[1].succeed($0)
XCTAssertEqual($0.state(), .closed)
}
// Ok, now fail the third channel future. Nothing bad should happen here.
ourChannelFutures[2].fail(DummyError())
// Verify that the first channel is the one listed as connected.
XCTAssertTrue((try ourChannelFutures.first!.futureResult.wait()) === (try channelFuture.wait()))
}
func testChannelCreationFails() throws {
var errors: [DummyError] = []
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80) { loop, _ in
errors.append(DummyError())
return loop.makeFailedFuture(errors.last!)
}
let channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the AAAA and A results return. We are going to fail the channel creation
// instantly, which should cause all 20 to appear.
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
XCTAssertEqual(errors.count, 10)
XCTAssertFalse(channelFuture.isFulfilled)
resolver.v4Promise.succeed(MANY_IPv4_RESULTS)
XCTAssertEqual(errors.count, 20)
XCTAssertTrue(channelFuture.isFulfilled)
if let error = channelFuture.getError() as? NIOConnectionError {
XCTAssertEqual(error.connectionErrors.map { $0.error as! DummyError }, errors)
} else {
XCTFail("Got unexpected error: \(String(describing: channelFuture.getError()))")
}
}
func testCancellationSyncWithConnectDelay() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .milliseconds(250)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the AAAA results return. Let the first connection attempt go out.
resolver.v6Promise.succeed(MANY_IPv6_RESULTS)
XCTAssertEqual(channels.count, 1)
// Advance time by 250 ms.
loop.advanceTime(by: .milliseconds(250))
// At this time the connection attempt should have failed, as the connect timeout
// fired.
XCTAssertThrowsError(try channelFuture.wait()) { error in
XCTAssertEqual(.connectTimeout(.milliseconds(250)), error as? ChannelError)
}
// There may be one or two channels, depending on ordering, but both
// should be closed.
XCTAssertTrue(channels.count == 1 || channels.count == 2, "Unexpected channel count: \(channels.count)")
for channel in channels {
XCTAssertEqual(channel.state(), .closed)
}
}
func testCancellationSyncWithResolutionDelay() throws {
var channels: [Channel] = []
defer {
channels.finishAll()
}
let (eyeballer, resolver, loop) = buildEyeballer(host: "example.com", port: 80, connectTimeout: .milliseconds(50)) {
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 channelFuture = eyeballer.resolveAndConnect()
let expectedQueries: [DummyResolver.Event] = [
.aaaa(host: "example.com", port: 80),
.a(host: "example.com", port: 80)
]
loop.run()
XCTAssertEqual(resolver.events, expectedQueries)
XCTAssertFalse(channelFuture.isFulfilled)
// Here the A results return. Let the first connection attempt go out.
resolver.v4Promise.succeed(MANY_IPv4_RESULTS)
XCTAssertEqual(channels.count, 0)
// Advance time by 50 ms.
loop.advanceTime(by: .milliseconds(50))
// At this time the connection attempt should have failed, as the connect timeout
// fired.
XCTAssertThrowsError(try channelFuture.wait()) { error in
XCTAssertEqual(.connectTimeout(.milliseconds(50)), error as? ChannelError)
}
// There may be zero or one channels, depending on ordering, but if there is one it
// should be closed
XCTAssertTrue(channels.count == 0 || channels.count == 1, "Unexpected channel count: \(channels.count)")
for channel in channels {
XCTAssertEqual(channel.state(), .closed)
}
}
}