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:
parent
73f758b5da
commit
5840333f0a
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue