Add a bind timeout. (#54)

Motivation:

In Network.framework it is possible for a bind operation to take a while
to complete if it ends up waiting for appropriate network conditions. As
a result, unlike in the POSIX case, we need to give users the ability
to configure the maximum amount of time they'd be willing to wait for a
bind call to succeed.

Modifications:

- Add a bind timeout.

Result:

Users won't have to wait forever.
This commit is contained in:
Cory Benfield 2019-08-06 11:28:44 +01:00 committed by GitHub
parent 6cba688855
commit 217f948d9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 60 additions and 16 deletions

View File

@ -57,5 +57,15 @@ public enum NIOTSErrors {
/// `UnableToResolveEndpoint` is thrown when an attempt is made to resolve a local endpoint, but /// `UnableToResolveEndpoint` is thrown when an attempt is made to resolve a local endpoint, but
/// insufficient information is available to create it. /// insufficient information is available to create it.
public struct UnableToResolveEndpoint: NIOTSError { } public struct UnableToResolveEndpoint: NIOTSError { }
/// `BindTimeout` is thrown when a timeout set for a `NWListenerBootstrap.bind` call has been exceeded
/// without successfully binding the address.
public struct BindTimeout: NIOTSError {
public var timeout: TimeAmount
public init(timeout: TimeAmount) {
self.timeout = timeout
}
}
} }
#endif #endif

View File

@ -31,6 +31,7 @@ public final class NIOTSListenerBootstrap {
private var childQoS: DispatchQoS? private var childQoS: DispatchQoS?
private var tcpOptions: NWProtocolTCP.Options = .init() private var tcpOptions: NWProtocolTCP.Options = .init()
private var tlsOptions: NWProtocolTLS.Options? private var tlsOptions: NWProtocolTLS.Options?
private var bindTimeout: TimeAmount?
/// Create a `NIOTSListenerBootstrap` for the `EventLoopGroup` `group`. /// Create a `NIOTSListenerBootstrap` for the `EventLoopGroup` `group`.
/// ///
@ -140,6 +141,14 @@ public final class NIOTSListenerBootstrap {
return self return self
} }
/// Specifies a timeout to apply to a bind attempt.
//
/// - parameters:
/// - timeout: The timeout that will apply to the bind attempt.
public func bindTimeout(_ timeout: TimeAmount) -> Self {
self.bindTimeout = timeout
return self
}
/// Specifies a QoS to use for the server channel, instead of the default QoS for the /// Specifies a QoS to use for the server channel, instead of the default QoS for the
/// event loop. /// event loop.
@ -186,18 +195,16 @@ public final class NIOTSListenerBootstrap {
/// - host: The host to bind on. /// - host: The host to bind on.
/// - port: The port to bind on. /// - port: The port to bind on.
public func bind(host: String, port: Int) -> EventLoopFuture<Channel> { public func bind(host: String, port: Int) -> EventLoopFuture<Channel> {
return self.bind0 { channel in return self.bind0 { (channel, promise) in
let p: EventLoopPromise<Void> = channel.eventLoop.makePromise()
do { do {
// NWListener does not actually resolve hostname-based NWEndpoints // NWListener does not actually resolve hostname-based NWEndpoints
// for use with requiredLocalEndpoint, so we fall back to // for use with requiredLocalEndpoint, so we fall back to
// SocketAddress for this. // SocketAddress for this.
let address = try SocketAddress.makeAddressResolvingHost(host, port: port) let address = try SocketAddress.makeAddressResolvingHost(host, port: port)
channel.bind(to: address, promise: p) channel.bind(to: address, promise: promise)
} catch { } catch {
p.fail(error) promise.fail(error)
} }
return p.futureResult
} }
} }
@ -206,8 +213,8 @@ public final class NIOTSListenerBootstrap {
/// - parameters: /// - parameters:
/// - address: The `SocketAddress` to bind on. /// - address: The `SocketAddress` to bind on.
public func bind(to address: SocketAddress) -> EventLoopFuture<Channel> { public func bind(to address: SocketAddress) -> EventLoopFuture<Channel> {
return self.bind0 { channel in return self.bind0 { (channel, promise) in
channel.bind(to: address) channel.bind(to: address, promise: promise)
} }
} }
@ -216,15 +223,13 @@ public final class NIOTSListenerBootstrap {
/// - parameters: /// - parameters:
/// - unixDomainSocketPath: The _Unix domain socket_ path to bind to. `unixDomainSocketPath` must not exist, it will be created by the system. /// - unixDomainSocketPath: The _Unix domain socket_ path to bind to. `unixDomainSocketPath` must not exist, it will be created by the system.
public func bind(unixDomainSocketPath: String) -> EventLoopFuture<Channel> { public func bind(unixDomainSocketPath: String) -> EventLoopFuture<Channel> {
return self.bind0 { channel in return self.bind0 { (channel, promise) in
let p: EventLoopPromise<Void> = channel.eventLoop.makePromise()
do { do {
let address = try SocketAddress(unixDomainSocketPath: unixDomainSocketPath) let address = try SocketAddress(unixDomainSocketPath: unixDomainSocketPath)
channel.bind(to: address, promise: p) channel.bind(to: address, promise: promise)
} catch { } catch {
p.fail(error) promise.fail(error)
} }
return p.futureResult
} }
} }
@ -233,12 +238,12 @@ public final class NIOTSListenerBootstrap {
/// - parameters: /// - parameters:
/// - endpoint: The `NWEndpoint` to bind this channel to. /// - endpoint: The `NWEndpoint` to bind this channel to.
public func bind(endpoint: NWEndpoint) -> EventLoopFuture<Channel> { public func bind(endpoint: NWEndpoint) -> EventLoopFuture<Channel> {
return self.bind0 { channel in return self.bind0 { (channel, promise) in
channel.triggerUserOutboundEvent(NIOTSNetworkEvents.BindToNWEndpoint(endpoint: endpoint)) channel.triggerUserOutboundEvent(NIOTSNetworkEvents.BindToNWEndpoint(endpoint: endpoint), promise: promise)
} }
} }
private func bind0(_ binder: @escaping (Channel) -> EventLoopFuture<Void>) -> EventLoopFuture<Channel> { private func bind0(_ binder: @escaping (Channel, EventLoopPromise<Void>) -> Void) -> EventLoopFuture<Channel> {
let eventLoop = self.group.next() as! NIOTSEventLoop let eventLoop = self.group.next() as! NIOTSEventLoop
let serverChannelInit = self.serverChannelInit ?? { _ in eventLoop.makeSucceededFuture(()) } let serverChannelInit = self.serverChannelInit ?? { _ in eventLoop.makeSucceededFuture(()) }
let childChannelInit = self.childChannelInit let childChannelInit = self.childChannelInit
@ -263,7 +268,20 @@ public final class NIOTSListenerBootstrap {
}.flatMap { }.flatMap {
serverChannel.register() serverChannel.register()
}.flatMap { }.flatMap {
binder(serverChannel) let bindPromise = eventLoop.makePromise(of: Void.self)
binder(serverChannel, bindPromise)
if let bindTimeout = self.bindTimeout {
let cancelTask = eventLoop.scheduleTask(in: bindTimeout) {
bindPromise.fail(NIOTSErrors.BindTimeout(timeout: bindTimeout))
serverChannel.close(promise: nil)
}
bindPromise.futureResult.whenComplete { (_: Result<Void, Error>) in
cancelTask.cancel()
}
}
return bindPromise.futureResult
}.map { }.map {
serverChannel as Channel serverChannel as Channel
}.flatMapError { error in }.flatMapError { error in

View File

@ -259,5 +259,21 @@ class NIOTSListenerChannelTests: XCTestCase {
XCTAssertEqual(promisedChannel.remoteAddress, connection.localAddress) XCTAssertEqual(promisedChannel.remoteAddress, connection.localAddress)
XCTAssertEqual(promisedChannel.localAddress, connection.remoteAddress) XCTAssertEqual(promisedChannel.localAddress, connection.remoteAddress)
} }
func testBindTimeout() throws {
// Testing the bind timeout is damn fiddly, because I don't know a reliable way to force it
// to happen. The best approach I can think of is to set the timeout to "now".
// If you see this test fail, verify that it isn't a simple timing issue first.
let listener = NIOTSListenerBootstrap(group: self.group)
.bindTimeout(.nanoseconds(0))
do {
let channel = try listener.bind(host: "localhost", port: 0).wait()
XCTAssertNoThrow(try channel.close().wait())
XCTFail("Did not throw")
} catch {
XCTAssertEqual(error as? NIOTSErrors.BindTimeout, NIOTSErrors.BindTimeout(timeout: .nanoseconds(0)), "unexpected error: \(error)")
}
}
} }
#endif #endif