Skip to content

Commit

Permalink
require ChannelOptions to be Equatable / ChannelOptions.Storage public
Browse files Browse the repository at this point in the history
Motivation:

ChannelOptions should've always been Equatable and so far we've hacked
around them not being Equatable when we wanted to compare them.

Modifications:

- make all ChannelOptions Equtable
- make ChannelOptions.Storage public

Result:

- ChannelOption comparison actually works
- fixes apple#598
  • Loading branch information
weissi committed Feb 14, 2019
1 parent f84b5b8 commit 5da4d97
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 85 deletions.
77 changes: 6 additions & 71 deletions Sources/NIO/Bootstrap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public final class ServerBootstrap {
private let childGroup: EventLoopGroup
private var serverChannelInit: ((Channel) -> EventLoopFuture<Void>)?
private var childChannelInit: ((Channel) -> EventLoopFuture<Void>)?
private var serverChannelOptions = ChannelOptionStorage()
private var childChannelOptions = ChannelOptionStorage()
private var serverChannelOptions = ChannelOptions.Storage()
private var childChannelOptions = ChannelOptions.Storage()

/// Create a `ServerBootstrap` for the `EventLoopGroup` `group`.
///
Expand Down Expand Up @@ -236,9 +236,9 @@ public final class ServerBootstrap {
public typealias InboundIn = SocketChannel

private let childChannelInit: ((Channel) -> EventLoopFuture<Void>)?
private let childChannelOptions: ChannelOptionStorage
private let childChannelOptions: ChannelOptions.Storage

init(childChannelInitializer: ((Channel) -> EventLoopFuture<Void>)?, childChannelOptions: ChannelOptionStorage) {
init(childChannelInitializer: ((Channel) -> EventLoopFuture<Void>)?, childChannelOptions: ChannelOptions.Storage) {
self.childChannelInit = childChannelInitializer
self.childChannelOptions = childChannelOptions
}
Expand Down Expand Up @@ -346,7 +346,7 @@ public final class ClientBootstrap {

private let group: EventLoopGroup
private var channelInitializer: ((Channel) -> EventLoopFuture<Void>)?
private var channelOptions = ChannelOptionStorage()
private var channelOptions = ChannelOptions.Storage()
private var connectTimeout: TimeAmount = TimeAmount.seconds(10)
private var resolver: Resolver?

Expand Down Expand Up @@ -556,7 +556,7 @@ public final class DatagramBootstrap {

private let group: EventLoopGroup
private var channelInitializer: ((Channel) -> EventLoopFuture<Void>)?
private var channelOptions = ChannelOptionStorage()
private var channelOptions = ChannelOptions.Storage()

/// Create a `DatagramBootstrap` on the `EventLoopGroup` `group`.
///
Expand Down Expand Up @@ -671,68 +671,3 @@ public final class DatagramBootstrap {
}
}
}

/* for tests */ internal struct ChannelOptionStorage {
private var storage: [(Any, (Any, (Channel) -> (Any, Any) -> EventLoopFuture<Void>))] = []

mutating func put<K: ChannelOption & Equatable>(key: K, value: K.OptionType) {
return self.put(key: key, value: value, equalsFunc: ==)
}

// HACK: this function should go for NIO 2.0, all ChannelOptions should be equatable
mutating func put<K: ChannelOption>(key: K, value: K.OptionType) {
if K.self == SocketOption.self {
return self.put(key: key as! SocketOption, value: value as! SocketOptionValue) { lhs, rhs in
switch (lhs, rhs) {
case (.const(let lLevel, let lName), .const(let rLevel, let rName)):
return lLevel == rLevel && lName == rName
}
}
} else {
return self.put(key: key, value: value) { _, _ in true }
}
}

mutating func put<K: ChannelOption>(key: K,
value newValue: K.OptionType,
equalsFunc: (K, K) -> Bool) {
func applier(_ t: Channel) -> (Any, Any) -> EventLoopFuture<Void> {
return { (x, y) in
return t.setOption(option: x as! K, value: y as! K.OptionType)
}
}
var hasSet = false
self.storage = self.storage.map { typeAndValue in
let (type, value) = typeAndValue
if type is K && equalsFunc(type as! K, key) {
hasSet = true
return (key, (newValue, applier))
} else {
return (type, value)
}
}
if !hasSet {
self.storage.append((key, (newValue, applier)))
}
}

func applyAll(channel: Channel) -> EventLoopFuture<Void> {
let applyPromise = channel.eventLoop.makePromise(of: Void.self)
var it = self.storage.makeIterator()

func applyNext() {
guard let (key, (value, applier)) = it.next() else {
// If we reached the end, everything is applied.
applyPromise.succeed(())
return
}

applier(channel)(key, value).map {
applyNext()
}.cascadeFailure(to: applyPromise)
}
applyNext()

return applyPromise.futureResult
}
}
132 changes: 131 additions & 1 deletion Sources/NIO/ChannelOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
//===----------------------------------------------------------------------===//

/// A configuration option that can be set on a `Channel` to configure different behaviour.
public protocol ChannelOption {
public protocol ChannelOption: Equatable {
associatedtype AssociatedValueType
associatedtype OptionType

Expand Down Expand Up @@ -70,6 +70,13 @@ public enum SocketOption: ChannelOption {
return (level, name)
}
}

public static func == (lhs: SocketOption, rhs: SocketOption) -> Bool {
switch (lhs, rhs) {
case (.const(let lLevel, let lName), .const(let rLevel, let rName)):
return lLevel == rLevel && lName == rName
}
}
}

/// `AllocatorOption` allows to specify the `ByteBufferAllocator` to use.
Expand All @@ -78,6 +85,13 @@ public enum AllocatorOption: ChannelOption {
public typealias OptionType = ByteBufferAllocator

case const(())

public static func == (lhs: AllocatorOption, rhs: AllocatorOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// `RecvAllocatorOption` allows users to specify the `RecvByteBufferAllocator` to use.
Expand All @@ -86,6 +100,13 @@ public enum RecvAllocatorOption: ChannelOption {
public typealias OptionType = RecvByteBufferAllocator

case const(())

public static func == (lhs: RecvAllocatorOption, rhs: RecvAllocatorOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// `AutoReadOption` allows users to configure if a `Channel` should automatically call `Channel.read` again once all data was read from the transport or
Expand All @@ -95,6 +116,13 @@ public enum AutoReadOption: ChannelOption {
public typealias OptionType = Bool

case const(())

public static func == (lhs: AutoReadOption, rhs: AutoReadOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// `WriteSpinOption` allows users to configure the number of repetitions of a only partially successful write call before considering the `Channel` not writable.
Expand All @@ -105,6 +133,13 @@ public enum WriteSpinOption: ChannelOption {
public typealias OptionType = UInt

case const(())

public static func == (lhs: WriteSpinOption, rhs: WriteSpinOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// `MaxMessagesPerReadOption` allows users to configure the maximum number of read calls to the underlying transport are performed before wait again until
Expand All @@ -114,6 +149,13 @@ public enum MaxMessagesPerReadOption: ChannelOption {
public typealias OptionType = UInt

case const(())

public static func == (lhs: MaxMessagesPerReadOption, rhs: MaxMessagesPerReadOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// `BacklogOption` allows users to configure the `backlog` value as specified in `man 2 listen`. This is only useful for `ServerSocketChannel`s.
Expand All @@ -122,6 +164,13 @@ public enum BacklogOption: ChannelOption {
public typealias OptionType = Int32

case const(())

public static func == (lhs: BacklogOption, rhs: BacklogOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// The watermark used to detect when `Channel.isWritable` returns `true` or `false`.
Expand Down Expand Up @@ -163,6 +212,13 @@ public enum WriteBufferWaterMarkOption: ChannelOption {
public typealias OptionType = WriteBufferWaterMark

case const(())

public static func == (lhs: WriteBufferWaterMarkOption, rhs: WriteBufferWaterMarkOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// `ConnectTimeoutOption` allows users to configure the `TimeAmount` after which a connect will fail if it was not established in the meantime. May be
Expand All @@ -172,6 +228,13 @@ public enum ConnectTimeoutOption: ChannelOption {
public typealias OptionType = TimeAmount?

case const(())

public static func == (lhs: ConnectTimeoutOption, rhs: ConnectTimeoutOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// `AllowRemoteHalfClosureOption` allows users to configure whether the `Channel` will close itself when its remote
Expand All @@ -184,6 +247,13 @@ public enum AllowRemoteHalfClosureOption: ChannelOption {
public typealias OptionType = Bool

case const(())

public static func == (lhs: AllowRemoteHalfClosureOption, rhs: AllowRemoteHalfClosureOption) -> Bool {
switch (lhs, rhs) {
case (.const(()), .const(())):
return true
}
}
}

/// Provides `ChannelOption`s to be used with a `Channel`, `Bootstrap` or `ServerBootstrap`.
Expand Down Expand Up @@ -218,3 +288,63 @@ public struct ChannelOptions {
/// - seealso: `AllowRemoteHalfClosureOption`.
public static let allowRemoteHalfClosure = AllowRemoteHalfClosureOption.const(())
}

extension ChannelOptions {
/// A type-safe storage facility for `ChannelOption`s. You will only ever need this if you implement your own
/// `Channel` that needs to store `ChannelOption`s.
public struct Storage {
private var storage: [(Any, (Any, (Channel) -> (Any, Any) -> EventLoopFuture<Void>))] = []

/// Add `Options`, a `ChannelOption` to the `ChannelOptions.Storage`.
///
/// - parameters:
/// - key: the key for the option
/// - value: the value for the option
public mutating func put<Option: ChannelOption>(key: Option, value newValue: Option.OptionType) {
func applier(_ t: Channel) -> (Any, Any) -> EventLoopFuture<Void> {
return { (x, y) in
return t.setOption(option: x as! Option, value: y as! Option.OptionType)
}
}
var hasSet = false
self.storage = self.storage.map { typeAndValue in
let (type, value) = typeAndValue
if type is Option && type as! Option == key {
hasSet = true
return (key, (newValue, applier))
} else {
return (type, value)
}
}
if !hasSet {
self.storage.append((key, (newValue, applier)))
}
}

/// Apply all stored `ChannelOption`s to `Channel`.
///
/// - parameters:
/// - channel: The `Channel` to apply the `ChannelOption`s to
/// - returns:
/// - An `EventLoopFuture` that is fulfilled when all `ChannelOption`s have been applied to the `Channel`.
public func applyAll(channel: Channel) -> EventLoopFuture<Void> {
let applyPromise = channel.eventLoop.makePromise(of: Void.self)
var it = self.storage.makeIterator()

func applyNext() {
guard let (key, (value, applier)) = it.next() else {
// If we reached the end, everything is applied.
applyPromise.succeed(())
return
}

applier(channel)(key, value).map {
applyNext()
}.cascadeFailure(to: applyPromise)
}
applyNext()

return applyPromise.futureResult
}
}
}
8 changes: 4 additions & 4 deletions Tests/NIOTests/ChannelOptionStorageTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import XCTest

class ChannelOptionStorageTest: XCTestCase {
func testWeStartWithNoOptions() throws {
let cos = ChannelOptionStorage()
let cos = ChannelOptions.Storage()
let optionsCollector = OptionsCollectingChannel()
XCTAssertNoThrow(try cos.applyAll(channel: optionsCollector).wait())
XCTAssertEqual(0, optionsCollector.allOptions.count)
}

func testSetTwoOptionsOfDifferentType() throws {
var cos = ChannelOptionStorage()
var cos = ChannelOptions.Storage()
let optionsCollector = OptionsCollectingChannel()
cos.put(key: ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
cos.put(key: ChannelOptions.backlog, value: 2)
Expand All @@ -36,7 +36,7 @@ class ChannelOptionStorageTest: XCTestCase {
func testSetTwoOptionsOfSameType() throws {
let options: [(SocketOption, SocketOptionValue)] = [(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), 1),
(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), 2)]
var cos = ChannelOptionStorage()
var cos = ChannelOptions.Storage()
let optionsCollector = OptionsCollectingChannel()
for kv in options {
cos.put(key: kv.0, value: kv.1)
Expand All @@ -50,7 +50,7 @@ class ChannelOptionStorageTest: XCTestCase {
}

func testSetOneOptionTwice() throws {
var cos = ChannelOptionStorage()
var cos = ChannelOptions.Storage()
let optionsCollector = OptionsCollectingChannel()
cos.put(key: ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
cos.put(key: ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 2)
Expand Down
9 changes: 0 additions & 9 deletions Tests/NIOTests/ChannelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2726,12 +2726,3 @@ fileprivate class VerifyConnectionFailureHandler: ChannelInboundHandler {
ctx.fireChannelUnregistered()
}
}

extension SocketOption: Equatable {
public static func == (lhs: SocketOption, rhs: SocketOption) -> Bool {
switch (lhs, rhs) {
case (.const(let lLevel, let lName), .const(let rLevel, let rName)):
return lLevel == rLevel && lName == rName
}
}
}
1 change: 1 addition & 0 deletions docs/public-api-changes-NIO1-to-NIO2.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@
- renamed `EventLoopFuture.hopTo(eventLoop:)` to `EventLoopFuture.hop(to:)`
- `EventLoopFuture.reduce(into:_:eventLoop:_:)` had its label signature changed to `EventLoopFuture.reduce(into:_:on:_:)`
- `EventLoopFuture.reduce(_:_:eventLoop:_:` had its label signature changed to `EventLoopFuture.reduce(_:_:on:_:)`
- all `ChannelOption`s are now required to be `Equatable`

0 comments on commit 5da4d97

Please sign in to comment.