bootstraps: offer ELG validation (#76)

Motivation:

Today, we just expect the ELGs passed to the bootstraps to be the
correct ones, if not, we crash.

Modifications:

Offer an alternative validatingGroup: init that just returns nil
if the ELGs are of the wrong types.

Result:

Easier to work with multi-stack systems for example when the user might
pass an ELG for either NIO on Sockets or NIO on Network.framework.

This is the NIOTS companion for https://github.com/apple/swift-nio/pull/1464
This commit is contained in:
Johannes Weiss 2020-04-03 15:57:03 +01:00 committed by GitHub
parent 7f98392c5d
commit 409fddd45c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 20 deletions

View File

@ -0,0 +1,23 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2020 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
//
//===----------------------------------------------------------------------===//
import NIO
/// Shared functionality across NIOTS bootstraps.
internal enum NIOTSBootstraps {
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
internal static func isCompatible(group: EventLoopGroup) -> Bool {
return group is NIOTSEventLoop || group is NIOTSEventLoopGroup
}
}

View File

@ -2,7 +2,7 @@
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Copyright (c) 2017-2020 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
@ -17,6 +17,27 @@ import NIO
import Dispatch
import Network
/// A `NIOTSConnectionBootstrap` is an easy way to bootstrap a `NIOTSConnectionChannel` when creating network clients.
///
/// Usually you re-use a `NIOTSConnectionBootstrap` once you set it up and called `connect` multiple times on it.
/// This way you ensure that the same `EventLoop`s will be shared across all your connections.
///
/// Example:
///
/// ```swift
/// let group = NIOTSEventLoopGroup()
/// defer {
/// try! group.syncShutdownGracefully()
/// }
/// let bootstrap = NIOTSConnectionBootstrap(group: group)
/// .channelInitializer { channel in
/// channel.pipeline.addHandler(MyChannelHandler())
/// }
/// try! bootstrap.connect(host: "example.org", port: 12345).wait()
/// /* the Channel is now connected */
/// ```
///
/// The connected `NIOTSConnectionChannel` will operate on `ByteBuffer` as inbound and on `IOData` as outbound messages.
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
public final class NIOTSConnectionBootstrap {
private let group: EventLoopGroup
@ -30,20 +51,20 @@ public final class NIOTSConnectionBootstrap {
/// Create a `NIOTSConnectionBootstrap` on the `EventLoopGroup` `group`.
///
/// This initializer only exists to be more in-line with the NIO core bootstraps, in that they
/// may be constructed with an `EventLoopGroup` and by extenstion an `EventLoop`. As such an
/// existing `NIOTSEventLoop` may be used to initialize this bootstrap. Where possible the
/// initializers accepting `NIOTSEventLoopGroup` should be used instead to avoid the wrong
/// type being used.
///
/// Note that the "real" solution is described in https://github.com/apple/swift-nio/issues/674.
/// The `EventLoopGroup` `group` must be compatible, otherwise the program will crash. `NIOTSConnectionBootstrap` is
/// compatible only with `NIOTSEventLoopGroup` as well as the `EventLoop`s returned by
/// `NIOTSEventLoopGroup.next`. See `init(validatingGroup:)` for a fallible initializer for
/// situations where it's impossible to tell ahead of time if the `EventLoopGroup` is compatible or not.
///
/// - parameters:
/// - group: The `EventLoopGroup` to use.
public init(group: EventLoopGroup) {
self.group = group
public convenience init(group: EventLoopGroup) {
guard NIOTSBootstraps.isCompatible(group: group) else {
preconditionFailure("NIOTSConnectionBootstrap is only compatible with NIOTSEventLoopGroup and " +
"NIOTSEventLoop. You tried constructing one with \(group) which is incompatible.")
}
self.channelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
self.init(validatingGroup: group)!
}
/// Create a `NIOTSConnectionBootstrap` on the `NIOTSEventLoopGroup` `group`.
@ -54,6 +75,20 @@ public final class NIOTSConnectionBootstrap {
self.init(group: group as EventLoopGroup)
}
/// Create a `NIOTSConnectionBootstrap` on the `NIOTSEventLoopGroup` `group`, validating
/// that the `EventLoopGroup` is compatible with `NIOTSConnectionBootstrap`.
///
/// - parameters:
/// - group: The `EventLoopGroup` to use.
public init?(validatingGroup group: EventLoopGroup) {
guard NIOTSBootstraps.isCompatible(group: group) else {
return nil
}
self.group = group
self.channelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
}
/// Initialize the connected `NIOTSConnectionChannel` with `initializer`. The most common task in initializer is to add
/// `ChannelHandler`s to the `ChannelPipeline`.
///

View File

@ -30,7 +30,7 @@ public protocol QoSEventLoop: EventLoop {
/// Submit a given task to be executed by the `EventLoop` at a given `qos`.
func execute(qos: DispatchQoS, _ task: @escaping () -> Void) -> Void
/// Schedule a `task` that is executed by this `SelectableEventLoop` after the given amount of time at the
/// Schedule a `task` that is executed by this `NIOTSEventLoop` after the given amount of time at the
/// given `qos`.
func scheduleTask<T>(in time: TimeAmount, qos: DispatchQoS, _ task: @escaping () throws -> T) -> Scheduled<T>
}

View File

@ -2,7 +2,7 @@
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Copyright (c) 2017-2020 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
@ -17,6 +17,42 @@ import NIO
import Dispatch
import Network
/// A `NIOTSListenerBootstrap` is an easy way to bootstrap a `NIOTSListenerChannel` when creating network servers.
///
/// Example:
///
/// ```swift
/// let group = NIOTSEventLoopGroup()
/// defer {
/// try! group.syncShutdownGracefully()
/// }
/// let bootstrap = NIOTSListenerBootstrap(group: group)
/// // Specify backlog and enable SO_REUSEADDR for the server itself
/// .serverChannelOption(ChannelOptions.backlog, value: 256)
/// .serverChannelOption(ChannelOptions.socketOption(.reuseaddr), value: 1)
///
/// // Set the handlers that are applied to the accepted child `Channel`s.
/// .childChannelInitializer { channel in
/// // Ensure we don't read faster then we can write by adding the BackPressureHandler into the pipeline.
/// channel.pipeline.addHandler(BackPressureHandler()).flatMap { () in
/// // make sure to instantiate your `ChannelHandlers` inside of
/// // the closure as it will be invoked once per connection.
/// channel.pipeline.addHandler(MyChannelHandler())
/// }
/// }
/// let channel = try! bootstrap.bind(host: host, port: port).wait()
/// /* the server will now be accepting connections */
///
/// try! channel.closeFuture.wait() // wait forever as we never close the Channel
/// ```
///
/// The `EventLoopFuture` returned by `bind` will fire with a `NIOTSListenerChannel`. This is the channel that owns the
/// listening socket. Each time it accepts a new connection it will fire a `NIOTSConnectionChannel` through the
/// `ChannelPipeline` via `fireChannelRead`: as a result, the `NIOTSListenerChannel` operates on `Channel`s as inbound
/// messages. Outbound messages are not supported on a `NIOTSListenerChannel` which means that each write attempt will
/// fail.
///
/// Accepted `NIOTSConnectionChannel`s operate on `ByteBuffer` as inbound data, and `IOData` as outbound data.
@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
public final class NIOTSListenerBootstrap {
private let group: EventLoopGroup
@ -42,18 +78,15 @@ public final class NIOTSListenerBootstrap {
/// Note that the "real" solution is described in https://github.com/apple/swift-nio/issues/674.
///
/// - parameters:
/// - group: The `EventLoopGroup` to use for the `ServerSocketChannel`.
/// - group: The `EventLoopGroup` to use for the `NIOTSListenerChannel`.
public convenience init(group: EventLoopGroup) {
self.init(group: group, childGroup: group)
self.serverChannelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
self.childChannelOptions.append(key: ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
}
/// Create a `NIOTSListenerBootstrap` for the `NIOTSEventLoopGroup` `group`.
///
/// - parameters:
/// - group: The `NIOTSEventLoopGroup` to use for the `ServerSocketChannel`.
/// - group: The `NIOTSEventLoopGroup` to use for the `NIOTSListenerChannel`.
public convenience init(group: NIOTSEventLoopGroup) {
self.init(group: group as EventLoopGroup)
}
@ -72,7 +105,29 @@ public final class NIOTSListenerBootstrap {
/// - group: The `EventLoopGroup` to use for the `bind` of the `NIOTSListenerChannel`
/// and to accept new `NIOTSConnectionChannel`s with.
/// - childGroup: The `EventLoopGroup` to run the accepted `NIOTSConnectionChannel`s on.
public init(group: EventLoopGroup, childGroup: EventLoopGroup) {
public convenience init(group: EventLoopGroup, childGroup: EventLoopGroup) {
guard NIOTSBootstraps.isCompatible(group: group) && NIOTSBootstraps.isCompatible(group: childGroup) else {
preconditionFailure("NIOTSListenerBootstrap is only compatible with NIOTSEventLoopGroup and " +
"NIOTSEventLoop. You tried constructing one with group: \(group) and " +
"childGroup: \(childGroup) at least one of which is incompatible.")
}
self.init(validatingGroup: group, childGroup: childGroup)!
}
/// Create a `NIOTSListenerBootstrap` on the `EventLoopGroup` `group` which accepts `Channel`s on `childGroup`,
/// validating that the `EventLoopGroup`s are compatible with `NIOTSListenerBootstrap`.
///
/// - parameters:
/// - group: The `EventLoopGroup` to use for the `bind` of the `NIOTSListenerChannel`
/// and to accept new `NIOTSConnectionChannel`s with.
/// - childGroup: The `EventLoopGroup` to run the accepted `NIOTSConnectionChannel`s on.
public init?(validatingGroup group: EventLoopGroup, childGroup: EventLoopGroup? = nil) {
let childGroup = childGroup ?? group
guard NIOTSBootstraps.isCompatible(group: group) && NIOTSBootstraps.isCompatible(group: childGroup) else {
return nil
}
self.group = group
self.childGroup = childGroup
@ -96,7 +151,7 @@ public final class NIOTSListenerBootstrap {
/// The `NIOTSListenerChannel` uses the accepted `NIOTSConnectionChannel`s as inbound messages.
///
/// - note: To set the initializer for the accepted `NIOTSConnectionChannel`s, look at
/// `ServerBootstrap.childChannelInitializer`.
/// `NIOTSConnectionBootstrap.childChannelInitializer`.
///
/// - parameters:
/// - initializer: A closure that initializes the provided `Channel`.

View File

@ -183,6 +183,69 @@ final class NIOTSBootstrapTests: XCTestCase {
XCTAssertNoThrow(XCTAssertFalse(try isTLSConnection1.futureResult.wait()))
XCTAssertNoThrow(XCTAssertTrue(try isTLSConnection2.futureResult.wait()))
}
func testNIOTSConnectionBootstrapValidatesWorkingELGsCorrectly() {
let elg = NIOTSEventLoopGroup()
defer {
XCTAssertNoThrow(try elg.syncShutdownGracefully())
}
let el = elg.next()
XCTAssertNotNil(NIOTSConnectionBootstrap(validatingGroup: elg))
XCTAssertNotNil(NIOTSConnectionBootstrap(validatingGroup: el))
}
func testNIOTSConnectionBootstrapRejectsNotWorkingELGsCorrectly() {
let elg = EmbeddedEventLoop()
defer {
XCTAssertNoThrow(try elg.syncShutdownGracefully())
}
let el = elg.next()
XCTAssertNil(NIOTSConnectionBootstrap(validatingGroup: elg))
XCTAssertNil(NIOTSConnectionBootstrap(validatingGroup: el))
}
func testNIOTSListenerBootstrapValidatesWorkingELGsCorrectly() {
let elg = NIOTSEventLoopGroup()
defer {
XCTAssertNoThrow(try elg.syncShutdownGracefully())
}
let el = elg.next()
XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: elg))
XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: el))
XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: elg, childGroup: elg))
XCTAssertNotNil(NIOTSListenerBootstrap(validatingGroup: el, childGroup: el))
}
func testNIOTSListenerBootstrapRejectsNotWorkingELGsCorrectly() {
let correctELG = NIOTSEventLoopGroup()
defer {
XCTAssertNoThrow(try correctELG.syncShutdownGracefully())
}
let wrongELG = EmbeddedEventLoop()
defer {
XCTAssertNoThrow(try wrongELG.syncShutdownGracefully())
}
let wrongEL = wrongELG.next()
let correctEL = correctELG.next()
// both wrong
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongELG))
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongEL))
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongELG, childGroup: wrongELG))
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongEL, childGroup: wrongEL))
// group correct, child group wrong
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: correctELG, childGroup: wrongELG))
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: correctEL, childGroup: wrongEL))
// group wrong, child group correct
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongELG, childGroup: correctELG))
XCTAssertNil(NIOTSListenerBootstrap(validatingGroup: wrongEL, childGroup: correctEL))
}
}
extension Channel {