Browse Source

Make clients sendable (#1386)

* Make clients sendable

Motivation:

gRPC clients should be 'Sendable'.

Modifications:

This commit contains a number of changes to enable clients to be
'Sendable'. The are:

- Require `GRPCChannel` to be 'Sendable' since the underlying API should
  be thread-safe.
- Require `GRPCClient` to be 'Sendable' as this just provides wrappers
  around a `GRPCChannel` which is also thread-safe. This propagates to
  generated client protocols which refine `GRPCClient`.
- Generated clients using the NIO API are classes (!) holding mutable
  call options and interceptor factories. Codegen was updated so make
  these `@unchecked Sendable` by protecting mutable state with a lock.
  Because this generates additional allocations and should never have
  been a class it is now marked deprecated in favor of a struct based
  client.
- `AnyServiceClient` is also a class and has been deprecated in favor of
  the struct based `GRPCAnyServiceClient`.
- A separate commit fixes these deprecation warnings.
- The test clients have also been marked as deprecated: they rely on an
  `EmbeddedChannel` which is not and will never be `Sendable` (without
  also being deprecated). The test clients are also marked
  as `@unchecked Sendable` to satisfy the requirement on `GRPCClient`
  despite being deprecated. The same is true for the `FakeChannel` (a
  `GRPCChannel`). We reccomend using a client and server on localhost as
  a replacement.
- Various supporting value types are marked as `Sendable`.

Result:

- Clients are now `Sendable`.
- Clients are now always `struct`s.
- Generated test clients are deprecated.

* Regenerate

* Fix deprecation warnings

* Use a typealias

* Client interceptors are preconcurrency and unchecked sendable
George Barnett 3 years ago
parent
commit
f7ea8d2dc5

+ 1 - 1
Sources/protoc-gen-grpc-swift/Generator-Client+AsyncAwait.swift

@@ -209,7 +209,7 @@ extension Generator {
   internal func printAsyncServiceClientImplementation() {
     self.printAvailabilityForAsyncAwait()
     self.withIndentation(
-      "\(self.access) struct \(self.asyncClientClassName): \(self.asyncClientProtocolName)",
+      "\(self.access) struct \(self.asyncClientStructName): \(self.asyncClientProtocolName)",
       braces: .curly
     ) {
       self.println("\(self.access) var channel: GRPCChannel")

+ 72 - 10
Sources/protoc-gen-grpc-swift/Generator-Client.swift

@@ -25,7 +25,9 @@ extension Generator {
       self.println()
       self.printClientProtocolExtension()
       self.println()
-      self.printServiceClientImplementation()
+      self.printClassBackedServiceClientImplementation()
+      self.println()
+      self.printStructBackedServiceClientImplementation()
       self.println()
       self.printIfCompilerGuardForAsyncAwait()
       self.printAsyncServiceClientProtocol()
@@ -157,7 +159,7 @@ extension Generator {
   }
 
   private func printServiceClientInterceptorFactoryProtocol() {
-    self.println("\(self.access) protocol \(self.clientInterceptorProtocolName) {")
+    self.println("\(self.access) protocol \(self.clientInterceptorProtocolName): GRPCSendable {")
     self.withIndentation {
       // Method specific interceptors.
       for method in service.methods {
@@ -186,10 +188,62 @@ extension Generator {
     )
   }
 
-  private func printServiceClientImplementation() {
+  private func printClassBackedServiceClientImplementation() {
+    self.printIfCompilerGuardForAsyncAwait()
+    self.println("@available(*, deprecated)")
+    self.println("extension \(clientClassName): @unchecked Sendable {}")
+    self.printEndCompilerGuardForAsyncAwait()
+    self.println()
+    self.println("@available(*, deprecated, renamed: \"\(clientStructName)\")")
     println("\(access) final class \(clientClassName): \(clientProtocolName) {")
     self.withIndentation {
+      println("private let lock = Lock()")
+      println("private var _defaultCallOptions: CallOptions")
+      println("private var _interceptors: \(clientInterceptorProtocolName)?")
+
       println("\(access) let channel: GRPCChannel")
+      println("\(access) var defaultCallOptions: CallOptions {")
+      self.withIndentation {
+        println("get { self.lock.withLock { return self._defaultCallOptions } }")
+        println("set { self.lock.withLockVoid { self._defaultCallOptions = newValue } }")
+      }
+      self.println("}")
+      println("\(access) var interceptors: \(clientInterceptorProtocolName)? {")
+      self.withIndentation {
+        println("get { self.lock.withLock { return self._interceptors } }")
+        println("set { self.lock.withLockVoid { self._interceptors = newValue } }")
+      }
+      println("}")
+      println()
+      println("/// Creates a client for the \(servicePath) service.")
+      println("///")
+      self.printParameters()
+      println("///   - channel: `GRPCChannel` to the service host.")
+      println(
+        "///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them."
+      )
+      println("///   - interceptors: A factory providing interceptors for each RPC.")
+      println("\(access) init(")
+      self.withIndentation {
+        println("channel: GRPCChannel,")
+        println("defaultCallOptions: CallOptions = CallOptions(),")
+        println("interceptors: \(clientInterceptorProtocolName)? = nil")
+      }
+      self.println(") {")
+      self.withIndentation {
+        println("self.channel = channel")
+        println("self._defaultCallOptions = defaultCallOptions")
+        println("self._interceptors = interceptors")
+      }
+      self.println("}")
+    }
+    println("}")
+  }
+
+  private func printStructBackedServiceClientImplementation() {
+    println("\(access) struct \(clientStructName): \(clientProtocolName) {")
+    self.withIndentation {
+      println("\(access) var channel: GRPCChannel")
       println("\(access) var defaultCallOptions: CallOptions")
       println("\(access) var interceptors: \(clientInterceptorProtocolName)?")
       println()
@@ -480,15 +534,23 @@ extension Generator {
   }
 
   private func printTestClient() {
-    self
-      .println(
-        "\(self.access) final class \(self.testClientClassName): \(self.clientProtocolName) {"
-      )
+    self.printIfCompilerGuardForAsyncAwait()
+    self.println("@available(swift, deprecated: 5.6)")
+    self.println("extension \(self.testClientClassName): @unchecked Sendable {}")
+    self.printEndCompilerGuardForAsyncAwait()
+    self.println()
+    self.println(
+      "@available(swift, deprecated: 5.6, message: \"Test clients are not Sendable "
+        + "but the 'GRPCClient' API requires clients to be Sendable. Using a localhost client and "
+        + "server is the recommended alternative.\")"
+    )
+    self.println(
+      "\(self.access) final class \(self.testClientClassName): \(self.clientProtocolName) {"
+    )
     self.withIndentation {
       self.println("private let fakeChannel: FakeChannel")
-      self.println("\(self.access) var defaultCallOptions: CallOptions")
-      self.println("\(self.access) var interceptors: \(self.clientInterceptorProtocolName)?")
-
+      self.println("\(access) var defaultCallOptions: CallOptions")
+      self.println("\(access) var interceptors: \(clientInterceptorProtocolName)?")
       self.println()
       self.println("\(self.access) var channel: GRPCChannel {")
       self.withIndentation {

+ 5 - 1
Sources/protoc-gen-grpc-swift/Generator-Names.swift

@@ -86,7 +86,11 @@ extension Generator {
     return nameForPackageService(file, service) + "Client"
   }
 
-  internal var asyncClientClassName: String {
+  internal var clientStructName: String {
+    return nameForPackageService(file, service) + "NIOClient"
+  }
+
+  internal var asyncClientStructName: String {
     return nameForPackageService(file, service) + "AsyncClient"
   }
 

+ 1 - 0
Sources/protoc-gen-grpc-swift/Generator.swift

@@ -136,6 +136,7 @@ class Generator {
     let moduleNames = [
       self.options.gRPCModuleName,
       "NIO",
+      "NIOConcurrencyHelpers",
       self.options.swiftProtobufModuleName,
     ]