فهرست منبع

Merge tag '1.7.1' into gb-update-async-await

George Barnett 3 سال پیش
والد
کامیت
4ca9bcf161

+ 1 - 1
CGRPCZlib.podspec

@@ -2,7 +2,7 @@ Pod::Spec.new do |s|
 
     s.name = 'CGRPCZlib'
     s.module_name = 'CGRPCZlib'
-    s.version = '1.5.0'
+    s.version = '1.6.0'
     s.license = { :type => 'Apache 2.0', :file => 'LICENSE' }
     s.summary = 'Compression library that provides in-memory compression and decompression functions'
     s.homepage = 'https://www.grpc.io'

+ 2 - 1
Package.swift

@@ -36,7 +36,7 @@ let packageDependencies: [Package.Dependency] = [
   ),
   .package(
     url: "https://github.com/apple/swift-nio-http2.git",
-    from: "1.18.2"
+    from: "1.19.2"
   ),
   .package(
     url: "https://github.com/apple/swift-nio-transport-services.git",
@@ -232,6 +232,7 @@ extension Target {
     name: "GRPCPerformanceTests",
     dependencies: [
       .grpc,
+      .grpcSampleData,
       .nioCore,
       .nioEmbedded,
       .nioPosix,

+ 7 - 1
README.md

@@ -130,9 +130,15 @@ the following line to your `Podfile`:
 ```ruby
     pod 'gRPC-Swift-Plugins'
 ```
-
 The plugins are available in the `Pods/gRPC-Swift-Plugins/` folder afterwards.
 
+#### Homebrew
+
+The plugins are available from [homebrew](https://brew.sh) and can be installed with:
+```bash
+    $ brew install swift-protobuf grpc-swift
+```
+
 ## Examples
 
 gRPC Swift has a number of tutorials and examples available. They are split

+ 1 - 0
Sources/GRPC/CallOptions.swift

@@ -133,6 +133,7 @@ extension CallOptions {
       self.source = source
     }
 
+    @usableFromInline
     internal func requestID() -> String? {
       switch self.source {
       case .none:

+ 1 - 1
Sources/GRPC/ConnectionManager.swift

@@ -591,7 +591,7 @@ internal final class ConnectionManager {
       ])
 
     case .connecting:
-      self.invalidState()
+      self.connectionFailed(withError: error)
 
     case var .active(state):
       state.error = error

+ 21 - 0
Sources/GRPC/ConnectionPool/PooledChannel.swift

@@ -132,6 +132,11 @@ internal final class PooledChannel: GRPCChannel {
     callOptions: CallOptions,
     interceptors: [ClientInterceptor<Request, Response>]
   ) -> Call<Request, Response> where Request: Message, Response: Message {
+    var callOptions = callOptions
+    if let requestID = callOptions.requestIDProvider.requestID() {
+      callOptions.applyRequestID(requestID)
+    }
+
     let (stream, eventLoop) = self._makeStreamChannel(callOptions: callOptions)
 
     return Call(
@@ -157,6 +162,11 @@ internal final class PooledChannel: GRPCChannel {
     callOptions: CallOptions,
     interceptors: [ClientInterceptor<Request, Response>]
   ) -> Call<Request, Response> where Request: GRPCPayload, Response: GRPCPayload {
+    var callOptions = callOptions
+    if let requestID = callOptions.requestIDProvider.requestID() {
+      callOptions.applyRequestID(requestID)
+    }
+
     let (stream, eventLoop) = self._makeStreamChannel(callOptions: callOptions)
 
     return Call(
@@ -192,3 +202,14 @@ internal final class PooledChannel: GRPCChannel {
     self._pool.shutdown(mode: .graceful(deadline), promise: promise)
   }
 }
+
+extension CallOptions {
+  @usableFromInline
+  mutating func applyRequestID(_ requestID: String) {
+    self.logger[metadataKey: MetadataKey.requestID] = "\(requestID)"
+    // Add the request ID header too.
+    if let requestIDHeader = self.requestIDHeader {
+      self.customMetadata.add(name: requestIDHeader, value: requestID)
+    }
+  }
+}

+ 16 - 6
Sources/GRPC/GRPCStatus.swift

@@ -105,10 +105,15 @@ public struct GRPCStatus: Error {
   ///   status code. Use `GRPCStatus.isOk` or check the code directly.
   public static let ok = GRPCStatus(code: .ok, message: nil)
   /// "Internal server error" status.
-  public static let processingError = GRPCStatus(
-    code: .internalError,
-    message: "unknown error processing request"
-  )
+  public static let processingError = Self.processingError(cause: nil)
+
+  public static func processingError(cause: Error?) -> GRPCStatus {
+    return GRPCStatus(
+      code: .internalError,
+      message: "unknown error processing request",
+      cause: cause
+    )
+  }
 }
 
 extension GRPCStatus: Equatable {
@@ -119,9 +124,14 @@ extension GRPCStatus: Equatable {
 
 extension GRPCStatus: CustomStringConvertible {
   public var description: String {
-    if let message = message {
+    switch (self.message, self.cause) {
+    case let (.some(message), .some(cause)):
+      return "\(self.code): \(message), cause: \(cause)"
+    case let (.some(message), .none):
       return "\(self.code): \(message)"
-    } else {
+    case let (.none, .some(cause)):
+      return "\(self.code), cause: \(cause)"
+    case (.none, .none):
       return "\(self.code)"
     }
   }

+ 5 - 5
Sources/GRPC/Interceptor/ClientTransport.swift

@@ -850,7 +850,7 @@ extension ClientTransport {
     promise: EventLoopPromise<Void>?
   ) {
     self.callEventLoop.assertInEventLoop()
-    self.logger.debug("buffering request part", metadata: [
+    self.logger.trace("buffering request part", metadata: [
       "request_part": "\(part.name)",
       "call_state": self.stateForLogging,
     ])
@@ -868,7 +868,7 @@ extension ClientTransport {
     // Save any flushing until we're done writing.
     var shouldFlush = false
 
-    self.logger.debug("unbuffering request parts", metadata: [
+    self.logger.trace("unbuffering request parts", metadata: [
       "request_parts": "\(self.writeBuffer.count)",
     ])
 
@@ -878,7 +878,7 @@ extension ClientTransport {
     while self.state.isUnbuffering, !self.writeBuffer.isEmpty {
       // Pull out as many writes as possible.
       while let write = self.writeBuffer.popFirst() {
-        self.logger.debug("unbuffering request part", metadata: [
+        self.logger.trace("unbuffering request part", metadata: [
           "request_part": "\(write.request.name)",
         ])
 
@@ -897,7 +897,7 @@ extension ClientTransport {
     }
 
     if self.writeBuffer.isEmpty {
-      self.logger.debug("request buffer drained")
+      self.logger.trace("request buffer drained")
     } else {
       self.logger.notice("unbuffering aborted", metadata: ["call_state": self.stateForLogging])
     }
@@ -914,7 +914,7 @@ extension ClientTransport {
   /// Fails any promises that come with buffered writes with `error`.
   /// - Parameter error: The `Error` to fail promises with.
   private func failBufferedWrites(with error: Error) {
-    self.logger.debug("failing buffered writes", metadata: ["call_state": self.stateForLogging])
+    self.logger.trace("failing buffered writes", metadata: ["call_state": self.stateForLogging])
 
     while let write = self.writeBuffer.popFirst() {
       write.promise?.fail(error)

+ 2 - 2
Sources/GRPC/ServerErrorProcessor.swift

@@ -44,7 +44,7 @@ internal enum ServerErrorProcessor {
       trailers = [:]
     } else {
       // Eh... well, we don't what status to use. Use a generic one.
-      status = .processingError
+      status = .processingError(cause: error)
       trailers = [:]
     }
 
@@ -84,7 +84,7 @@ internal enum ServerErrorProcessor {
       mergedTrailers = trailers
     } else {
       // Eh... well, we don't what status to use. Use a generic one.
-      status = .processingError
+      status = .processingError(cause: error)
       mergedTrailers = trailers
     }
 

+ 2 - 2
Sources/GRPC/Version.swift

@@ -19,10 +19,10 @@ internal enum Version {
   internal static let major = 1
 
   /// The minor version.
-  internal static let minor = 6
+  internal static let minor = 7
 
   /// The patch version.
-  internal static let patch = 0
+  internal static let patch = 1
 
   /// The version string.
   internal static let versionString = "\(major).\(minor).\(patch)-async-await.1"

+ 36 - 6
Sources/GRPCPerformanceTests/Benchmarks/ServerProvidingBenchmark.swift

@@ -14,26 +14,56 @@
  * limitations under the License.
  */
 import GRPC
+import GRPCSampleData
 import NIOCore
 import NIOPosix
 
 class ServerProvidingBenchmark: Benchmark {
   private let providers: [CallHandlerProvider]
   private let threadCount: Int
+  private let useNIOTSIfAvailable: Bool
+  private let useTLS: Bool
   private var group: EventLoopGroup!
   private(set) var server: Server!
 
-  init(providers: [CallHandlerProvider], threadCount: Int = 1) {
+  init(
+    providers: [CallHandlerProvider],
+    useNIOTSIfAvailable: Bool,
+    useTLS: Bool,
+    threadCount: Int = 1
+  ) {
     self.providers = providers
+    self.useNIOTSIfAvailable = useNIOTSIfAvailable
+    self.useTLS = useTLS
     self.threadCount = threadCount
   }
 
   func setUp() throws {
-    self.group = MultiThreadedEventLoopGroup(numberOfThreads: self.threadCount)
-    self.server = try Server.insecure(group: self.group)
-      .withServiceProviders(self.providers)
-      .bind(host: "127.0.0.1", port: 0)
-      .wait()
+    if self.useNIOTSIfAvailable {
+      self.group = PlatformSupport.makeEventLoopGroup(loopCount: self.threadCount)
+    } else {
+      self.group = MultiThreadedEventLoopGroup(numberOfThreads: self.threadCount)
+    }
+
+    if self.useTLS {
+      #if canImport(NIOSSL)
+      self.server = try Server.usingTLSBackedByNIOSSL(
+        on: self.group,
+        certificateChain: [SampleCertificate.server.certificate],
+        privateKey: SamplePrivateKey.server
+      ).withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
+        .withServiceProviders(self.providers)
+        .bind(host: "127.0.0.1", port: 0)
+        .wait()
+      #else
+      fatalError("NIOSSL must be imported to use TLS")
+      #endif
+    } else {
+      self.server = try Server.insecure(group: self.group)
+        .withServiceProviders(self.providers)
+        .bind(host: "127.0.0.1", port: 0)
+        .wait()
+    }
   }
 
   func tearDown() throws {

+ 40 - 7
Sources/GRPCPerformanceTests/Benchmarks/UnaryThroughput.swift

@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 import GRPC
+import GRPCSampleData
 import NIOCore
 import NIOPosix
 
@@ -22,23 +23,50 @@ import NIOPosix
 /// Requests are sent in batches of (up-to) 100 requests. This is due to
 /// https://github.com/apple/swift-nio-http2/issues/87#issuecomment-483542401.
 class Unary: ServerProvidingBenchmark {
+  private let useNIOTSIfAvailable: Bool
+  private let useTLS: Bool
   private var group: EventLoopGroup!
   private(set) var client: Echo_EchoClient!
 
   let requestCount: Int
   let requestText: String
 
-  init(requests: Int, text: String) {
+  init(requests: Int, text: String, useNIOTSIfAvailable: Bool, useTLS: Bool) {
+    self.useNIOTSIfAvailable = useNIOTSIfAvailable
+    self.useTLS = useTLS
     self.requestCount = requests
     self.requestText = text
-    super.init(providers: [MinimalEchoProvider()])
+    super.init(
+      providers: [MinimalEchoProvider()],
+      useNIOTSIfAvailable: useNIOTSIfAvailable,
+      useTLS: useTLS
+    )
   }
 
   override func setUp() throws {
     try super.setUp()
-    self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
-    let channel = ClientConnection.insecure(group: self.group)
-      .connect(host: "127.0.0.1", port: self.server.channel.localAddress!.port!)
+
+    if self.useNIOTSIfAvailable {
+      self.group = PlatformSupport.makeEventLoopGroup(loopCount: 1)
+    } else {
+      self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+    }
+
+    let channel: ClientConnection
+
+    if self.useTLS {
+      #if canImport(NIOSSL)
+      channel = ClientConnection.usingTLSBackedByNIOSSL(on: self.group)
+        .withTLS(trustRoots: .certificates([SampleCertificate.ca.certificate]))
+        .withTLS(serverHostnameOverride: "localhost")
+        .connect(host: "127.0.0.1", port: self.server.channel.localAddress!.port!)
+      #else
+      fatalError("NIOSSL must be imported to use TLS")
+      #endif
+    } else {
+      channel = ClientConnection.insecure(group: self.group)
+        .connect(host: "127.0.0.1", port: self.server.channel.localAddress!.port!)
+    }
 
     self.client = .init(channel: channel)
   }
@@ -72,9 +100,14 @@ class Unary: ServerProvidingBenchmark {
 class Bidi: Unary {
   let batchSize: Int
 
-  init(requests: Int, text: String, batchSize: Int) {
+  init(requests: Int, text: String, batchSize: Int, useNIOTSIfAvailable: Bool, useTLS: Bool) {
     self.batchSize = batchSize
-    super.init(requests: requests, text: text)
+    super.init(
+      requests: requests,
+      text: text,
+      useNIOTSIfAvailable: useNIOTSIfAvailable,
+      useTLS: useTLS
+    )
   }
 
   override func run() throws -> Int {

+ 65 - 9
Sources/GRPCPerformanceTests/main.swift

@@ -24,31 +24,59 @@ let largeRequest = String(repeating: "x", count: 1 << 16) // 65k
 func runBenchmarks(spec: TestSpec) {
   measureAndPrint(
     description: "unary_10k_small_requests",
-    benchmark: Unary(requests: 10000, text: smallRequest),
+    benchmark: Unary(
+      requests: 10000,
+      text: smallRequest,
+      useNIOTSIfAvailable: spec.useNIOTransportServices,
+      useTLS: spec.useTLS
+    ),
     spec: spec
   )
 
   measureAndPrint(
     description: "unary_10k_long_requests",
-    benchmark: Unary(requests: 10000, text: largeRequest),
+    benchmark: Unary(
+      requests: 10000,
+      text: largeRequest,
+      useNIOTSIfAvailable: spec.useNIOTransportServices,
+      useTLS: spec.useTLS
+    ),
     spec: spec
   )
 
   measureAndPrint(
     description: "bidi_10k_small_requests_in_batches_of_1",
-    benchmark: Bidi(requests: 10000, text: smallRequest, batchSize: 1),
+    benchmark: Bidi(
+      requests: 10000,
+      text: smallRequest,
+      batchSize: 1,
+      useNIOTSIfAvailable: spec.useNIOTransportServices,
+      useTLS: spec.useTLS
+    ),
     spec: spec
   )
 
   measureAndPrint(
     description: "bidi_10k_small_requests_in_batches_of_5",
-    benchmark: Bidi(requests: 10000, text: smallRequest, batchSize: 5),
+    benchmark: Bidi(
+      requests: 10000,
+      text: smallRequest,
+      batchSize: 5,
+      useNIOTSIfAvailable: spec.useNIOTransportServices,
+      useTLS: spec.useTLS
+    ),
     spec: spec
   )
 
   measureAndPrint(
     description: "bidi_1k_large_requests_in_batches_of_5",
-    benchmark: Bidi(requests: 1000, text: largeRequest, batchSize: 1),
+    benchmark: Bidi(
+      requests: 1000,
+      text: largeRequest,
+      batchSize: 1,
+      useNIOTSIfAvailable: spec.useNIOTransportServices,
+      useTLS: spec.useTLS
+    ),
     spec: spec
   )
 
@@ -153,10 +181,14 @@ func runBenchmarks(spec: TestSpec) {
 struct TestSpec {
   var action: Action
   var repeats: Int
+  var useNIOTransportServices: Bool
+  var useTLS: Bool
 
-  init(action: Action, repeats: Int = 10) {
+  init(action: Action, repeats: Int, useNIOTransportServices: Bool, useTLS: Bool) {
     self.action = action
     self.repeats = repeats
+    self.useNIOTransportServices = useNIOTransportServices
+    self.useTLS = useTLS
   }
 
   enum Action {
@@ -190,6 +222,15 @@ struct PerformanceTests: ParsableCommand {
   @Flag(name: .shortAndLong, help: "Run all tests")
   var all: Bool = false
 
+  @Flag(help: "Use NIO Transport Services (if available)")
+  var useNIOTransportServices: Bool = false
+
+  @Flag(help: "Use TLS for tests which support it")
+  var useTLS: Bool = false
+
+  @Option(help: "The number of times to run each test")
+  var repeats: Int = 10
+
   @Argument(help: "The tests to run")
   var tests: [String] = []
 
@@ -197,11 +238,26 @@ struct PerformanceTests: ParsableCommand {
     let spec: TestSpec
 
     if self.list {
-      spec = TestSpec(action: .list)
+      spec = TestSpec(
+        action: .list,
+        repeats: self.repeats,
+        useNIOTransportServices: self.useNIOTransportServices,
+        useTLS: self.useTLS
+      )
     } else if self.all {
-      spec = TestSpec(action: .run(.all))
+      spec = TestSpec(
+        action: .run(.all),
+        repeats: self.repeats,
+        useNIOTransportServices: self.useNIOTransportServices,
+        useTLS: self.useTLS
+      )
     } else {
-      spec = TestSpec(action: .run(.some(self.tests)))
+      spec = TestSpec(
+        action: .run(.some(self.tests)),
+        repeats: self.repeats,
+        useNIOTransportServices: self.useNIOTransportServices,
+        useTLS: self.useTLS
+      )
     }
 
     runBenchmarks(spec: spec)

+ 22 - 0
Tests/GRPCTests/ConnectionManagerTests.swift

@@ -1120,6 +1120,28 @@ extension ConnectionManagerTests {
 
     XCTAssertEqual(http2.streamsClosed, 4)
   }
+
+  func testChannelErrorWhenConnecting() throws {
+    let channelPromise = self.loop.makePromise(of: Channel.self)
+    let manager = self.makeConnectionManager { _, _ in
+      return channelPromise.futureResult
+    }
+
+    let multiplexer: EventLoopFuture<HTTP2StreamMultiplexer> = self.waitForStateChange(
+      from: .idle,
+      to: .connecting
+    ) {
+      let channel = manager.getHTTP2Multiplexer()
+      self.loop.run()
+      return channel
+    }
+
+    self.waitForStateChange(from: .connecting, to: .shutdown) {
+      manager.channelError(EventLoopError.shutdown)
+    }
+
+    XCTAssertThrowsError(try multiplexer.wait())
+  }
 }
 
 internal struct Change: Hashable, CustomStringConvertible {

+ 54 - 0
Tests/GRPCTests/EchoHelpers/Providers/MetadataEchoProvider.swift

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021, 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 EchoModel
+import GRPC
+import NIOCore
+
+internal final class MetadataEchoProvider: Echo_EchoProvider {
+  let interceptors: Echo_EchoServerInterceptorFactoryProtocol? = nil
+
+  func get(
+    request: Echo_EchoRequest,
+    context: StatusOnlyCallContext
+  ) -> EventLoopFuture<Echo_EchoResponse> {
+    let response = Echo_EchoResponse.with {
+      $0.text = context.headers.sorted(by: { $0.name < $1.name }).map {
+        $0.name + ": " + $0.value
+      }.joined(separator: "\n")
+    }
+
+    return context.eventLoop.makeSucceededFuture(response)
+  }
+
+  func expand(
+    request: Echo_EchoRequest,
+    context: StreamingResponseCallContext<Echo_EchoResponse>
+  ) -> EventLoopFuture<GRPCStatus> {
+    return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unimplemented))
+  }
+
+  func collect(
+    context: UnaryResponseCallContext<Echo_EchoResponse>
+  ) -> EventLoopFuture<(StreamEvent<Echo_EchoRequest>) -> Void> {
+    return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unimplemented))
+  }
+
+  func update(
+    context: StreamingResponseCallContext<Echo_EchoResponse>
+  ) -> EventLoopFuture<(StreamEvent<Echo_EchoRequest>) -> Void> {
+    return context.eventLoop.makeFailedFuture(GRPCStatus(code: .unimplemented))
+  }
+}

+ 31 - 1
Tests/GRPCTests/GRPCStatusTests.swift

@@ -34,7 +34,7 @@ class GRPCStatusTests: GRPCTestCase {
     )
   }
 
-  func testStatusDescriptionWithMessage() {
+  func testStatusDescriptionWithWithMessageWithoutCause() {
     XCTAssertEqual(
       "ok (0): OK",
       String(describing: GRPCStatus(code: .ok, message: "OK"))
@@ -51,6 +51,36 @@ class GRPCStatusTests: GRPCTestCase {
     )
   }
 
+  func testStatusDescriptionWithMessageWithCause() {
+    struct UnderlyingError: Error, CustomStringConvertible {
+      var description: String { "underlying error description" }
+    }
+    let cause = UnderlyingError()
+    XCTAssertEqual(
+      "internal error (13): unknown error processing request, cause: \(cause.description)",
+      String(describing: GRPCStatus(
+        code: .internalError,
+        message: "unknown error processing request",
+        cause: cause
+      ))
+    )
+  }
+
+  func testStatusDescriptionWithoutMessageWithCause() {
+    struct UnderlyingError: Error, CustomStringConvertible {
+      var description: String { "underlying error description" }
+    }
+    let cause = UnderlyingError()
+    XCTAssertEqual(
+      "internal error (13), cause: \(cause.description)",
+      String(describing: GRPCStatus(
+        code: .internalError,
+        message: nil,
+        cause: cause
+      ))
+    )
+  }
+
   func testCoWSemanticsModifyingMessage() {
     let nilStorageID = GRPCStatus.ok.testingOnly_storageObjectIdentifier
     var status = GRPCStatus(code: .resourceExhausted)

+ 85 - 0
Tests/GRPCTests/RequestIDTests.swift

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021, 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 EchoModel
+import GRPC
+import NIOCore
+import NIOPosix
+import XCTest
+
+internal final class RequestIDTests: GRPCTestCase {
+  private var server: Server!
+  private var group: EventLoopGroup!
+
+  override func setUp() {
+    super.setUp()
+
+    self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
+    self.server = try! Server.insecure(group: self.group)
+      .withServiceProviders([MetadataEchoProvider()])
+      .withLogger(self.serverLogger)
+      .bind(host: "127.0.0.1", port: 0)
+      .wait()
+  }
+
+  override func tearDown() {
+    XCTAssertNoThrow(try self.server.close().wait())
+    XCTAssertNoThrow(try self.group.syncShutdownGracefully())
+    super.tearDown()
+  }
+
+  func testRequestIDIsPopulatedClientConnection() throws {
+    let channel = ClientConnection.insecure(group: self.group)
+      .connect(host: "127.0.0.1", port: self.server.channel.localAddress!.port!)
+
+    defer {
+      let loop = group.next()
+      let promise = loop.makePromise(of: Void.self)
+      channel.closeGracefully(deadline: .now() + .seconds(30), promise: promise)
+      XCTAssertNoThrow(try promise.futureResult.wait())
+    }
+
+    try self._testRequestIDIsPopulated(channel: channel)
+  }
+
+  func testRequestIDIsPopulatedChannelPool() throws {
+    let channel = try! GRPCChannelPool.with(
+      target: .host("127.0.0.1", port: self.server.channel.localAddress!.port!),
+      transportSecurity: .plaintext,
+      eventLoopGroup: self.group
+    )
+
+    defer {
+      let loop = group.next()
+      let promise = loop.makePromise(of: Void.self)
+      channel.closeGracefully(deadline: .now() + .seconds(30), promise: promise)
+      XCTAssertNoThrow(try promise.futureResult.wait())
+    }
+
+    try self._testRequestIDIsPopulated(channel: channel)
+  }
+
+  func _testRequestIDIsPopulated(channel: GRPCChannel) throws {
+    let echo = Echo_EchoClient(channel: channel)
+    let options = CallOptions(
+      requestIDProvider: .userDefined("foo"),
+      requestIDHeader: "request-id-header"
+    )
+
+    let get = echo.get(.with { $0.text = "ignored" }, callOptions: options)
+    let response = try get.response.wait()
+    XCTAssert(response.text.contains("request-id-header: foo"))
+  }
+}

+ 3 - 2
Tests/GRPCTests/ZeroLengthWriteTests.swift

@@ -67,7 +67,7 @@ final class ZeroLengthWriteTests: GRPCTestCase {
     return try self.serverBuilder(group: group, secure: secure, debugInitializer: debugInitializer)
       .withServiceProviders([self.makeEchoProvider()])
       .withLogger(self.serverLogger)
-      .bind(host: "localhost", port: 0)
+      .bind(host: "127.0.0.1", port: 0)
       .wait()
   }
 
@@ -79,7 +79,8 @@ final class ZeroLengthWriteTests: GRPCTestCase {
   ) throws -> ClientConnection {
     return self.clientBuilder(group: group, secure: secure, debugInitializer: debugInitializer)
       .withBackgroundActivityLogger(self.clientLogger)
-      .connect(host: "localhost", port: port)
+      .withConnectionReestablishment(enabled: false)
+      .connect(host: "127.0.0.1", port: port)
   }
 
   func makeEchoProvider() -> Echo_EchoProvider { return EchoProvider() }

+ 1 - 1
gRPC-Swift-Plugins.podspec

@@ -1,7 +1,7 @@
 Pod::Spec.new do |s|
 
     s.name = 'gRPC-Swift-Plugins'
-    s.version = '1.5.0'
+    s.version = '1.6.0'
     s.license = { :type => 'Apache 2.0', :file => 'LICENSE' }
     s.summary = 'Swift gRPC code generator plugin binaries'
     s.homepage = 'https://www.grpc.io'

+ 1 - 1
gRPC-Swift.podspec

@@ -2,7 +2,7 @@ Pod::Spec.new do |s|
 
     s.name = 'gRPC-Swift'
     s.module_name = 'GRPC'
-    s.version = '1.5.0'
+    s.version = '1.6.0'
     s.license = { :type => 'Apache 2.0', :file => 'LICENSE' }
     s.summary = 'Swift gRPC code generator plugin and runtime library'
     s.homepage = 'https://www.grpc.io'