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:
parent
6cba688855
commit
217f948d9b
|
|
@ -57,5 +57,15 @@ public enum NIOTSErrors {
|
|||
/// `UnableToResolveEndpoint` is thrown when an attempt is made to resolve a local endpoint, but
|
||||
/// insufficient information is available to create it.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public final class NIOTSListenerBootstrap {
|
|||
private var childQoS: DispatchQoS?
|
||||
private var tcpOptions: NWProtocolTCP.Options = .init()
|
||||
private var tlsOptions: NWProtocolTLS.Options?
|
||||
private var bindTimeout: TimeAmount?
|
||||
|
||||
/// Create a `NIOTSListenerBootstrap` for the `EventLoopGroup` `group`.
|
||||
///
|
||||
|
|
@ -140,6 +141,14 @@ public final class NIOTSListenerBootstrap {
|
|||
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
|
||||
/// event loop.
|
||||
|
|
@ -186,18 +195,16 @@ public final class NIOTSListenerBootstrap {
|
|||
/// - host: The host to bind on.
|
||||
/// - port: The port to bind on.
|
||||
public func bind(host: String, port: Int) -> EventLoopFuture<Channel> {
|
||||
return self.bind0 { channel in
|
||||
let p: EventLoopPromise<Void> = channel.eventLoop.makePromise()
|
||||
return self.bind0 { (channel, promise) in
|
||||
do {
|
||||
// NWListener does not actually resolve hostname-based NWEndpoints
|
||||
// for use with requiredLocalEndpoint, so we fall back to
|
||||
// SocketAddress for this.
|
||||
let address = try SocketAddress.makeAddressResolvingHost(host, port: port)
|
||||
channel.bind(to: address, promise: p)
|
||||
channel.bind(to: address, promise: promise)
|
||||
} catch {
|
||||
p.fail(error)
|
||||
promise.fail(error)
|
||||
}
|
||||
return p.futureResult
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -206,8 +213,8 @@ public final class NIOTSListenerBootstrap {
|
|||
/// - parameters:
|
||||
/// - address: The `SocketAddress` to bind on.
|
||||
public func bind(to address: SocketAddress) -> EventLoopFuture<Channel> {
|
||||
return self.bind0 { channel in
|
||||
channel.bind(to: address)
|
||||
return self.bind0 { (channel, promise) in
|
||||
channel.bind(to: address, promise: promise)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -216,15 +223,13 @@ public final class NIOTSListenerBootstrap {
|
|||
/// - parameters:
|
||||
/// - 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> {
|
||||
return self.bind0 { channel in
|
||||
let p: EventLoopPromise<Void> = channel.eventLoop.makePromise()
|
||||
return self.bind0 { (channel, promise) in
|
||||
do {
|
||||
let address = try SocketAddress(unixDomainSocketPath: unixDomainSocketPath)
|
||||
channel.bind(to: address, promise: p)
|
||||
channel.bind(to: address, promise: promise)
|
||||
} catch {
|
||||
p.fail(error)
|
||||
promise.fail(error)
|
||||
}
|
||||
return p.futureResult
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,12 +238,12 @@ public final class NIOTSListenerBootstrap {
|
|||
/// - parameters:
|
||||
/// - endpoint: The `NWEndpoint` to bind this channel to.
|
||||
public func bind(endpoint: NWEndpoint) -> EventLoopFuture<Channel> {
|
||||
return self.bind0 { channel in
|
||||
channel.triggerUserOutboundEvent(NIOTSNetworkEvents.BindToNWEndpoint(endpoint: endpoint))
|
||||
return self.bind0 { (channel, promise) in
|
||||
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 serverChannelInit = self.serverChannelInit ?? { _ in eventLoop.makeSucceededFuture(()) }
|
||||
let childChannelInit = self.childChannelInit
|
||||
|
|
@ -263,7 +268,20 @@ public final class NIOTSListenerBootstrap {
|
|||
}.flatMap {
|
||||
serverChannel.register()
|
||||
}.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 {
|
||||
serverChannel as Channel
|
||||
}.flatMapError { error in
|
||||
|
|
|
|||
|
|
@ -259,5 +259,21 @@ class NIOTSListenerChannelTests: XCTestCase {
|
|||
XCTAssertEqual(promisedChannel.remoteAddress, connection.localAddress)
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue