Browse Source

Add support for NIOTransportServices (#485)

* Update Pacakge.swift

* Add support for NIOTransportServices

* Add more availability checks

* Avoid running certain tests on Linux

* Move functional tests from an extension into the class

* Don't prefer .networkFramework on Linux

* Remove whitespace

* Rename Generic{Client,Server}Bootstrap to {Client,Server}BootstrapProtocol
George Barnett 6 years ago
parent
commit
dbf410201f

+ 9 - 0
Package.resolved

@@ -46,6 +46,15 @@
           "version": "2.1.0"
         }
       },
+      {
+        "package": "swift-nio-transport-services",
+        "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git",
+        "state": {
+          "branch": null,
+          "revision": "d59370d89af5e647fa14779b693cb5378602aa6d",
+          "version": "1.0.3"
+        }
+      },
       {
         "package": "SwiftProtobuf",
         "repositoryURL": "https://github.com/apple/swift-protobuf.git",

+ 6 - 1
Package.swift

@@ -33,6 +33,9 @@ let package = Package(
     .package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.2.1"),
     // TLS via SwiftNIO
     .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"),
+    // Support for Network.framework where possible. Note: from 1.0.2 the package
+    // is essentially an empty import on platforms where it isn't supported.
+    .package(url: "https://github.com/apple/swift-nio-transport-services.git", from: "1.0.2"),
 
     // Official SwiftProtobuf library, for [de]serializing data to send on the wire.
     .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.5.0"),
@@ -47,6 +50,7 @@ let package = Package(
       dependencies: [
         "NIO",
         "NIOFoundationCompat",
+        "NIOTransportServices",
         "NIOHTTP1",
         "NIOHTTP2",
         "NIOSSL",
@@ -116,4 +120,5 @@ let package = Package(
       ],
       path: "Sources/Examples/Echo"
     ),
-  ])
+  ]
+)

+ 3 - 3
Sources/GRPC/ClientConnection.swift

@@ -60,8 +60,8 @@ open class ClientConnection {
   /// handlers detailed in the documentation for `ClientConnection`.
   ///
   /// - Parameter configuration: The configuration to prepare the bootstrap with.
-  public class func makeBootstrap(configuration: Configuration) -> ClientBootstrap {
-    let bootstrap = ClientBootstrap(group: configuration.eventLoopGroup)
+  public class func makeBootstrap(configuration: Configuration) -> ClientBootstrapProtocol {
+    let bootstrap = GRPCNIO.makeClientBootstrap(group: configuration.eventLoopGroup)
       // Enable SO_REUSEADDR and TCP_NODELAY.
       .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
       .channelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
@@ -274,7 +274,7 @@ extension ClientConnection {
 
 // MARK: - Configuration helpers/extensions
 
-fileprivate extension ClientBootstrap {
+fileprivate extension ClientBootstrapProtocol {
   /// Connect to the given connection target.
   ///
   /// - Parameter target: The target to connect to.

+ 178 - 0
Sources/GRPC/GRPCNIO.swift

@@ -0,0 +1,178 @@
+/*
+ * Copyright 2019, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import NIO
+import NIOTransportServices
+
+/// How a network implementation should be chosen.
+public enum NetworkPreference {
+  /// Use the best available, that is, Network.framework (and NIOTransportServices) when it is
+  /// available on Darwin platforms (macOS 10.14+, iOS 12.0+, tvOS 12.0+, watchOS 6.0+), and
+  /// falling back to the POSIX network model otherwise.
+  case best
+
+  /// Use the given implementation. Doing so may require additional availability checks depending
+  /// on the implementation.
+  case userDefined(NetworkImplementation)
+}
+
+/// The network implementation to use: POSIX sockets or Network.framework. This also determines
+/// which variant of NIO to use; NIO or NIOTransportServices, respectively.
+public enum NetworkImplementation {
+#if canImport(Network)
+  /// Network.framework (NIOTransportServices).
+  @available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
+  case networkFramework
+#endif
+  /// POSIX (NIO).
+  case posix
+}
+
+extension NetworkPreference {
+  /// The network implementation, and by extension the NIO variant which will be used.
+  ///
+  /// Network.framework is available on macOS 10.14+, iOS 12.0+, tvOS 12.0+ and watchOS 6.0+.
+  ///
+  /// This isn't directly useful when implementing code which branches on the network preference
+  /// since that code will still need the appropriate availability check:
+  ///
+  /// - `@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)`, or
+  /// - `#available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)`.
+  public var implementation: NetworkImplementation {
+    switch self {
+    case .best:
+      #if canImport(Network)
+      guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else {
+        // This is gated by the availability of `.networkFramework` so should never happen.
+        fatalError(".networkFramework is being used on an unsupported platform")
+      }
+      return .networkFramework
+      #else
+      return .posix
+      #endif
+
+    case .userDefined(let implementation):
+      return implementation
+    }
+  }
+}
+
+// MARK: - Generic Bootstraps
+
+// TODO: Revisit the handling of NIO/NIOTS once https://github.com/apple/swift-nio/issues/796
+// is addressed.
+
+/// This protocol is intended as a layer of abstraction over `ClientBootstrap` and
+/// `NIOTSConnectionBootstrap`.
+public protocol ClientBootstrapProtocol {
+  func connect(to: SocketAddress) -> EventLoopFuture<Channel>
+  func connect(host: String, port: Int) -> EventLoopFuture<Channel>
+  func connect(unixDomainSocketPath: String) -> EventLoopFuture<Channel>
+
+  func connectTimeout(_ timeout: TimeAmount) -> Self
+  func channelOption<T>(_ option: T, value: T.Value) -> Self where T: ChannelOption
+  func channelInitializer(_ handler: @escaping (Channel) -> EventLoopFuture<Void>) -> Self
+}
+
+extension ClientBootstrap: ClientBootstrapProtocol {}
+
+#if canImport(Network)
+@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
+extension NIOTSConnectionBootstrap: ClientBootstrapProtocol {}
+#endif
+
+/// This protocol is intended as a layer of abstraction over `ServerBootstrap` and
+/// `NIOTSListenerBootstrap`.
+public protocol ServerBootstrapProtocol {
+  func bind(to: SocketAddress) -> EventLoopFuture<Channel>
+  func bind(host: String, port: Int) -> EventLoopFuture<Channel>
+  func bind(unixDomainSocketPath: String) -> EventLoopFuture<Channel>
+
+  func serverChannelInitializer(_ initializer: @escaping (Channel) -> EventLoopFuture<Void>) -> Self
+  func serverChannelOption<T>(_ option: T, value: T.Value) -> Self where T: ChannelOption
+
+  func childChannelInitializer(_ initializer: @escaping (Channel) -> EventLoopFuture<Void>) -> Self
+  func childChannelOption<T>(_ option: T, value: T.Value) -> Self where T: ChannelOption
+}
+
+extension ServerBootstrap: ServerBootstrapProtocol {}
+
+#if canImport(Network)
+@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
+extension NIOTSListenerBootstrap: ServerBootstrapProtocol {}
+#endif
+
+// MARK: - Bootstrap / EventLoopGroup helpers
+
+public enum GRPCNIO {
+  /// Makes a new event loop group based on the network preference.
+  ///
+  /// If `.best` is chosen and `Network.framework` is available then `NIOTSEventLoopGroup` will
+  /// be returned. A `MultiThreadedEventLoopGroup` will be returned otherwise.
+  ///
+  /// - Parameter loopCount: The number of event loops to create in the event loop group.
+  /// - Parameter networkPreference: Network prefernce; defaulting to `.best`.
+  public static func makeEventLoopGroup(
+    loopCount: Int,
+    networkPreference: NetworkPreference = .best
+  ) -> EventLoopGroup {
+    switch networkPreference.implementation {
+#if canImport(Network)
+    case .networkFramework:
+      guard #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) else {
+        // This is gated by the availability of `.networkFramework` so should never happen.
+        fatalError(".networkFramework is being used on an unsupported platform")
+      }
+      return NIOTSEventLoopGroup(loopCount: loopCount)
+#endif
+    case .posix:
+      return MultiThreadedEventLoopGroup(numberOfThreads: loopCount)
+    }
+  }
+
+  /// Makes a new client bootstrap using the given `EventLoopGroup`.
+  ///
+  /// If the `EventLoopGroup` is a `NIOTSEventLoopGroup` then the returned bootstrap will be a
+  /// `NIOTSConnectionBootstrap`, otherwise it will be a `ClientBootstrap`.
+  ///
+  /// - Parameter group: The `EventLoopGroup` to use.
+  public static func makeClientBootstrap(group: EventLoopGroup) -> ClientBootstrapProtocol {
+#if canImport(Network)
+    if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
+      if let tsGroup = group as? NIOTSEventLoopGroup {
+        return NIOTSConnectionBootstrap(group: tsGroup)
+      }
+    }
+#endif
+    return ClientBootstrap(group: group)
+  }
+
+  /// Makes a new server bootstrap using the given `EventLoopGroup`.
+  ///
+  /// If the `EventLoopGroup` is a `NIOTSEventLoopGroup` then the returned bootstrap will be a
+  /// `NIOTSListenerBootstrap`, otherwise it will be a `ServerBootstrap`.
+  ///
+  /// - Parameter group: The `EventLoopGroup` to use.
+  public static func makeServerBootstrap(group: EventLoopGroup) -> ServerBootstrapProtocol {
+#if canImport(Network)
+    if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
+      if let tsGroup = group as? NIOTSEventLoopGroup {
+        return NIOTSListenerBootstrap(group: tsGroup)
+      }
+    }
+#endif
+    return ServerBootstrap(group: group)
+  }
+}

+ 9 - 1
Tests/GRPCTests/BasicEchoTestCase.swift

@@ -117,6 +117,12 @@ class EchoTestCaseBase: XCTestCase {
   var server: Server!
   var client: Echo_EchoServiceClient!
 
+  // Prefer POSIX: subclasses can override this and add availability checks to ensure NIOTS
+  // variants run where possible.
+  var networkPreference: NetworkPreference {
+    return .userDefined(.posix)
+  }
+
   func makeClientConfiguration() throws -> ClientConnection.Configuration {
     return .init(
       target: .hostAndPort("localhost", 5050),
@@ -152,7 +158,9 @@ class EchoTestCaseBase: XCTestCase {
     self.serverEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
     self.server = try! self.makeServer()
 
-    self.clientEventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+    self.clientEventLoopGroup = GRPCNIO.makeEventLoopGroup(
+      loopCount: 1,
+      networkPreference: self.networkPreference)
     self.client = try! self.makeEchoClient()
   }
 

+ 105 - 8
Tests/GRPCTests/FunctionalTests.swift

@@ -35,9 +35,7 @@ class FunctionalTestsInsecureTransport: EchoTestCaseBase {
       String(describing: $0)
     }
   }
-}
 
-extension FunctionalTestsInsecureTransport {
   func doTestUnary(request: Echo_EchoRequest, expect response: Echo_EchoResponse, file: StaticString = #file, line: UInt = #line) {
     let responseExpectation = self.makeResponseExpectation()
     let statusExpectation = self.makeStatusExpectation()
@@ -108,9 +106,7 @@ extension FunctionalTestsInsecureTransport {
   func testUnaryEmptyRequest() throws {
     self.doTestUnary(request: Echo_EchoRequest(), expect: Echo_EchoResponse(text: "Swift echo get: "))
   }
-}
 
-extension FunctionalTestsInsecureTransport {
   func doTestClientStreaming(messages: [String], file: StaticString = #file, line: UInt = #line) throws {
     let responseExpectation = self.makeResponseExpectation()
     let statusExpectation = self.makeStatusExpectation()
@@ -136,9 +132,7 @@ extension FunctionalTestsInsecureTransport {
     self.defaultTestTimeout = 15.0
     XCTAssertNoThrow(try doTestClientStreaming(messages: lotsOfStrings))
   }
-}
 
-extension FunctionalTestsInsecureTransport {
   func doTestServerStreaming(messages: [String], file: StaticString = #file, line: UInt = #line) throws {
     let responseExpectation = self.makeResponseExpectation(expectedFulfillmentCount: messages.count)
     let statusExpectation = self.makeStatusExpectation()
@@ -165,9 +159,7 @@ extension FunctionalTestsInsecureTransport {
     self.defaultTestTimeout = 15.0
     XCTAssertNoThrow(try doTestServerStreaming(messages: lotsOfStrings))
   }
-}
 
-extension FunctionalTestsInsecureTransport {
   private func doTestBidirectionalStreaming(messages: [String], waitForEachResponse: Bool = false, file: StaticString = #file, line: UInt = #line) throws {
     let responseExpectation = self.makeResponseExpectation(expectedFulfillmentCount: messages.count)
     let statusExpectation = self.makeStatusExpectation()
@@ -226,3 +218,108 @@ class FunctionalTestsMutualAuthentication: FunctionalTestsInsecureTransport {
     return .mutualAuthentication
   }
 }
+
+// MARK: - Variants using NIO TS and Network.framework
+
+// Unfortunately `swift test --generate-linuxmain` uses the macOS test discovery. Because of this
+// it's difficult to avoid tests which run on Linux. To get around this shortcoming we can just
+// run no-op tests on Linux.
+@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
+class FunctionalTestsInsecureTransportNIOTS: FunctionalTestsInsecureTransport {
+  override var networkPreference: NetworkPreference {
+    #if canImport(Network)
+    return .userDefined(.networkFramework)
+    #else
+    // We shouldn't need this, since the tests won't do anything. However, we still need to be able
+    // to compile this class.
+    return .userDefined(.posix)
+    #endif
+  }
+
+  override func testBidirectionalStreamingBatched() throws {
+    #if canImport(Network)
+    try super.testBidirectionalStreamingBatched()
+    #endif
+  }
+
+  override func testBidirectionalStreamingLotsOfMessagesBatched() throws {
+    #if canImport(Network)
+    try super.testBidirectionalStreamingLotsOfMessagesBatched()
+    #endif
+  }
+
+  override func testBidirectionalStreamingLotsOfMessagesPingPong() throws {
+    #if canImport(Network)
+    try super.testBidirectionalStreamingLotsOfMessagesPingPong()
+    #endif
+  }
+
+  override func testBidirectionalStreamingPingPong() throws {
+    #if canImport(Network)
+    try super.testBidirectionalStreamingPingPong()
+    #endif
+  }
+
+  override func testClientStreaming() {
+    #if canImport(Network)
+    super.testClientStreaming()
+    #endif
+  }
+
+  override func testClientStreamingLotsOfMessages() throws {
+    #if canImport(Network)
+    try super.testClientStreamingLotsOfMessages()
+    #endif
+  }
+
+  override func testServerStreaming() {
+    #if canImport(Network)
+    super.testServerStreaming()
+    #endif
+  }
+
+  override func testServerStreamingLotsOfMessages() {
+    #if canImport(Network)
+    super.testServerStreamingLotsOfMessages()
+    #endif
+  }
+
+  override func testUnary() throws {
+    #if canImport(Network)
+    try super.testUnary()
+    #endif
+  }
+
+  override func testUnaryEmptyRequest() throws {
+    #if canImport(Network)
+    try super.testUnaryEmptyRequest()
+    #endif
+  }
+
+  override func testUnaryLotsOfRequests() throws {
+    #if canImport(Network)
+    try super.testUnaryLotsOfRequests()
+    #endif
+  }
+
+  override func testUnaryWithLargeData() throws {
+    #if canImport(Network)
+    try super.testUnaryWithLargeData()
+    #endif
+  }
+
+}
+
+@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
+class FunctionalTestsAnonymousClientNIOTS: FunctionalTestsInsecureTransportNIOTS {
+  override var transportSecurity: TransportSecurity {
+    return .anonymousClient
+  }
+}
+
+@available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *)
+class FunctionalTestsMutualAuthenticationNIOTS: FunctionalTestsInsecureTransportNIOTS {
+  override var transportSecurity: TransportSecurity {
+    return .mutualAuthentication
+  }
+}

+ 63 - 0
Tests/GRPCTests/XCTestManifests.swift

@@ -121,6 +121,26 @@ extension FunctionalTestsAnonymousClient {
     ]
 }
 
+extension FunctionalTestsAnonymousClientNIOTS {
+    // DO NOT MODIFY: This is autogenerated, use:
+    //   `swift test --generate-linuxmain`
+    // to regenerate.
+    static let __allTests__FunctionalTestsAnonymousClientNIOTS = [
+        ("testBidirectionalStreamingBatched", testBidirectionalStreamingBatched),
+        ("testBidirectionalStreamingLotsOfMessagesBatched", testBidirectionalStreamingLotsOfMessagesBatched),
+        ("testBidirectionalStreamingLotsOfMessagesPingPong", testBidirectionalStreamingLotsOfMessagesPingPong),
+        ("testBidirectionalStreamingPingPong", testBidirectionalStreamingPingPong),
+        ("testClientStreaming", testClientStreaming),
+        ("testClientStreamingLotsOfMessages", testClientStreamingLotsOfMessages),
+        ("testServerStreaming", testServerStreaming),
+        ("testServerStreamingLotsOfMessages", testServerStreamingLotsOfMessages),
+        ("testUnary", testUnary),
+        ("testUnaryEmptyRequest", testUnaryEmptyRequest),
+        ("testUnaryLotsOfRequests", testUnaryLotsOfRequests),
+        ("testUnaryWithLargeData", testUnaryWithLargeData),
+    ]
+}
+
 extension FunctionalTestsInsecureTransport {
     // DO NOT MODIFY: This is autogenerated, use:
     //   `swift test --generate-linuxmain`
@@ -141,6 +161,26 @@ extension FunctionalTestsInsecureTransport {
     ]
 }
 
+extension FunctionalTestsInsecureTransportNIOTS {
+    // DO NOT MODIFY: This is autogenerated, use:
+    //   `swift test --generate-linuxmain`
+    // to regenerate.
+    static let __allTests__FunctionalTestsInsecureTransportNIOTS = [
+        ("testBidirectionalStreamingBatched", testBidirectionalStreamingBatched),
+        ("testBidirectionalStreamingLotsOfMessagesBatched", testBidirectionalStreamingLotsOfMessagesBatched),
+        ("testBidirectionalStreamingLotsOfMessagesPingPong", testBidirectionalStreamingLotsOfMessagesPingPong),
+        ("testBidirectionalStreamingPingPong", testBidirectionalStreamingPingPong),
+        ("testClientStreaming", testClientStreaming),
+        ("testClientStreamingLotsOfMessages", testClientStreamingLotsOfMessages),
+        ("testServerStreaming", testServerStreaming),
+        ("testServerStreamingLotsOfMessages", testServerStreamingLotsOfMessages),
+        ("testUnary", testUnary),
+        ("testUnaryEmptyRequest", testUnaryEmptyRequest),
+        ("testUnaryLotsOfRequests", testUnaryLotsOfRequests),
+        ("testUnaryWithLargeData", testUnaryWithLargeData),
+    ]
+}
+
 extension FunctionalTestsMutualAuthentication {
     // DO NOT MODIFY: This is autogenerated, use:
     //   `swift test --generate-linuxmain`
@@ -161,6 +201,26 @@ extension FunctionalTestsMutualAuthentication {
     ]
 }
 
+extension FunctionalTestsMutualAuthenticationNIOTS {
+    // DO NOT MODIFY: This is autogenerated, use:
+    //   `swift test --generate-linuxmain`
+    // to regenerate.
+    static let __allTests__FunctionalTestsMutualAuthenticationNIOTS = [
+        ("testBidirectionalStreamingBatched", testBidirectionalStreamingBatched),
+        ("testBidirectionalStreamingLotsOfMessagesBatched", testBidirectionalStreamingLotsOfMessagesBatched),
+        ("testBidirectionalStreamingLotsOfMessagesPingPong", testBidirectionalStreamingLotsOfMessagesPingPong),
+        ("testBidirectionalStreamingPingPong", testBidirectionalStreamingPingPong),
+        ("testClientStreaming", testClientStreaming),
+        ("testClientStreamingLotsOfMessages", testClientStreamingLotsOfMessages),
+        ("testServerStreaming", testServerStreaming),
+        ("testServerStreamingLotsOfMessages", testServerStreamingLotsOfMessages),
+        ("testUnary", testUnary),
+        ("testUnaryEmptyRequest", testUnaryEmptyRequest),
+        ("testUnaryLotsOfRequests", testUnaryLotsOfRequests),
+        ("testUnaryWithLargeData", testUnaryWithLargeData),
+    ]
+}
+
 extension GRPCChannelHandlerTests {
     // DO NOT MODIFY: This is autogenerated, use:
     //   `swift test --generate-linuxmain`
@@ -352,8 +412,11 @@ public func __allTests() -> [XCTestCaseEntry] {
         testCase(ClientTimeoutTests.__allTests__ClientTimeoutTests),
         testCase(ConnectionBackoffTests.__allTests__ConnectionBackoffTests),
         testCase(FunctionalTestsAnonymousClient.__allTests__FunctionalTestsAnonymousClient),
+        testCase(FunctionalTestsAnonymousClientNIOTS.__allTests__FunctionalTestsAnonymousClientNIOTS),
         testCase(FunctionalTestsInsecureTransport.__allTests__FunctionalTestsInsecureTransport),
+        testCase(FunctionalTestsInsecureTransportNIOTS.__allTests__FunctionalTestsInsecureTransportNIOTS),
         testCase(FunctionalTestsMutualAuthentication.__allTests__FunctionalTestsMutualAuthentication),
+        testCase(FunctionalTestsMutualAuthenticationNIOTS.__allTests__FunctionalTestsMutualAuthenticationNIOTS),
         testCase(GRPCChannelHandlerTests.__allTests__GRPCChannelHandlerTests),
         testCase(GRPCInsecureInteroperabilityTests.__allTests__GRPCInsecureInteroperabilityTests),
         testCase(GRPCSecureInteroperabilityTests.__allTests__GRPCSecureInteroperabilityTests),