Support opting-out of Network.framework waiting state. (#15)

Motivation:

In some cases users may prefer not to wait for Network.framework to
reattempt connection in the future. Users should be able to opt-out of the
default waiting behaviour in those cases.

Modifications:

- Added WaitForActivity ChannelOption.

Result:

Users can configure channels better.
This commit is contained in:
Cory Benfield 2018-12-03 11:28:20 +00:00 committed by Johannes Weiss
parent 73f758b5da
commit 5840333f0a
3 changed files with 123 additions and 2 deletions

View File

@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 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
// swift-tools-version:4.0
//
// swift-tools-version:4.0
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIO
/// `NIOTSWaitForActivityOption` controls whether the `Channel` should wait for connection changes
/// during the connection process if the connection attempt fails. If Network.framework believes that
/// a connection may succeed in future, it may transition into the `.waiting` state. By default, this option
/// is set to `true` and NIO allows this state transition, though it does count time in that state against
/// the timeout. If this option is set to `false`, transitioning into this state will be treated the same as
/// transitioning into the `failed` state, causing immediate connection failure.
///
/// This option is only valid with `NIOTSConnectionBootstrap`.
public enum NIOTSWaitForActivityOption: ChannelOption {
public typealias AssociatedValueType = ()
public typealias OptionType = Bool
case const(())
}
/// Options that can be set explicitly and only on bootstraps provided by `NIOTransportServices`.
public struct NIOTSChannelOptions {
/// - seealso: `NIOTSWaitForActivityOption`.
public static let waitForActivity = NIOTSWaitForActivityOption.const(())
}

View File

@ -53,6 +53,9 @@ private struct ConnectionChannelOptions {
/// Whether we support remote half closure. If not true, remote half closure will
/// cause connection drops.
internal var supportRemoteHalfClosure: Bool = false
/// Whether this channel should wait for the connection to become active.
internal var waitForActivity: Bool = true
}
@ -304,6 +307,14 @@ extension NIOTSConnectionChannel: Channel {
if self.backpressureManager.writabilityChanges(whenUpdatingWaterMarks: value as! WriteBufferWaterMark) {
self.pipeline.fireChannelWritabilityChanged()
}
case _ as NIOTSWaitForActivityOption:
let newValue = value as! Bool
self.options.waitForActivity = newValue
if let state = self.nwConnection?.state, case .waiting(let err) = state {
// We're in waiting now, so we should drop the connection.
self.close0(error: err, mode: .all, promise: nil)
}
default:
fatalError("option \(type(of: option)).\(option) not supported")
}
@ -345,6 +356,8 @@ extension NIOTSConnectionChannel: Channel {
}
case _ as WriteBufferWaterMarkOption:
return self.backpressureManager.waterMarks as! T.OptionType
case _ as NIOTSWaitForActivityOption:
return self.options.waitForActivity as! T.OptionType
default:
fatalError("option \(type(of: option)).\(option) not supported")
}
@ -605,9 +618,9 @@ extension NIOTSConnectionChannel {
case .setup:
preconditionFailure("Should not be told about this state.")
case .waiting(let err):
if case .activating = self.state {
if case .activating = self.state, self.options.waitForActivity {
// This means the connection cannot currently be completed. We should notify the pipeline
// here, or support this with a channel option or something, but for now for the same of
// here, or support this with a channel option or something, but for now for the sake of
// demos we will just allow ourselves into this stage.
break
}

View File

@ -17,6 +17,7 @@ import XCTest
import Network
import NIO
import NIOTransportServices
import Foundation
final class ConnectRecordingHandler: ChannelOutboundHandler {
@ -68,6 +69,17 @@ final class WritabilityChangedHandler: ChannelInboundHandler {
}
final class DisableWaitingAfterConnect: ChannelOutboundHandler {
typealias OutboundIn = Any
typealias OutboundOut = Any
func connect(ctx: ChannelHandlerContext, to address: SocketAddress, promise: EventLoopPromise<Void>?) {
ctx.connect(to: address, promise: promise)
ctx.channel.setOption(option: NIOTSChannelOptions.waitForActivity, value: false)
}
}
class NIOTSConnectionChannelTests: XCTestCase {
private var group: NIOTSEventLoopGroup!
@ -487,4 +499,61 @@ class NIOTSConnectionChannelTests: XCTestCase {
XCTFail("Unexpected error")
}
}
func testEarlyExitForWaitingChannel() throws {
let connectFuture = NIOTSConnectionBootstrap(group: self.group)
.channelOption(NIOTSChannelOptions.waitForActivity, value: false)
.connect(to: try SocketAddress(unixDomainSocketPath: "/this/path/definitely/doesnt/exist"))
do {
let conn = try connectFuture.wait()
XCTAssertNoThrow(try conn.close().wait())
XCTFail("Did not throw")
} catch is NWError {
// fine
} catch {
XCTFail("Unexpected error \(error)")
}
}
func testEarlyExitCanBeSetInWaitingState() throws {
let connectFuture = NIOTSConnectionBootstrap(group: self.group)
.channelInitializer { channel in
channel.pipeline.add(handler: DisableWaitingAfterConnect())
}.connect(to: try SocketAddress(unixDomainSocketPath: "/this/path/definitely/doesnt/exist"))
do {
let conn = try connectFuture.wait()
XCTAssertNoThrow(try conn.close().wait())
XCTFail("Did not throw")
} catch is NWError {
// fine
} catch {
XCTFail("Unexpected error \(error)")
}
}
func testCanObserveValueOfDisableWaiting() throws {
let listener = try NIOTSListenerBootstrap(group: self.group)
.bind(host: "localhost", port: 0).wait()
defer {
XCTAssertNoThrow(try listener.close().wait())
}
let connectFuture = NIOTSConnectionBootstrap(group: self.group)
.channelInitializer { channel in
return channel.getOption(option: NIOTSChannelOptions.waitForActivity).map { value in
XCTAssertTrue(value)
}.then {
channel.setOption(option: NIOTSChannelOptions.waitForActivity, value: false)
}.then {
channel.getOption(option: NIOTSChannelOptions.waitForActivity)
}.map { value in
XCTAssertFalse(value)
}
}.connect(to: listener.localAddress!)
let conn = try connectFuture.wait()
XCTAssertNoThrow(try conn.close().wait())
}
}