ソースを参照

Move CompressionAlgorithm to core (#1858)

Motivation:

`CallOptions` only let you enable/disable compression, it doesn't allow
you to control which algorithm should be used. This is an unnecessary
limitation. This was done because `CompressionAlgorithm` lives in the
http2 module.

Modifications:

- Move `CompressionAlgorithm` to the core module
- Rename 'identity' to 'none' as that's clearer for users
- Add extensions in the http2 module to create an algorithm from its
  name
- Add a `CompressionAlgorithmSet` type which uses an option set which
  allows for cheaper updates.
- Update call options

Result:

- `CallOptions` is more flexible
- Updating the call options set is cheaper
George Barnett 1 年間 前
コミット
6dd9ae8c78

+ 1 - 1
Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift

@@ -35,7 +35,7 @@ final class GRPCClientStreamHandler: ChannelDuplexHandler {
     methodDescriptor: MethodDescriptor,
     scheme: Scheme,
     outboundEncoding: CompressionAlgorithm,
-    acceptedEncodings: [CompressionAlgorithm],
+    acceptedEncodings: CompressionAlgorithmSet,
     maximumPayloadSize: Int,
     skipStateMachineAssertions: Bool = false
   ) {

+ 28 - 27
Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift

@@ -14,39 +14,40 @@
  * limitations under the License.
  */
 
-/// Supported message compression algorithms.
-///
-/// These algorithms are indicated in the "grpc-encoding" header. As such, a lack of "grpc-encoding"
-/// header indicates that there is no message compression.
-public struct CompressionAlgorithm: Hashable, Sendable {
-  /// Identity compression; "no" compression but indicated via the "grpc-encoding" header.
-  public static let identity = CompressionAlgorithm(.identity)
-  public static let deflate = CompressionAlgorithm(.deflate)
-  public static let gzip = CompressionAlgorithm(.gzip)
+@_spi(Package) import GRPCCore
 
-  // The order here is important: most compression to least.
-  public static let all: [CompressionAlgorithm] = [.gzip, .deflate, .identity]
-
-  public var name: String {
-    return self.algorithm.rawValue
+extension CompressionAlgorithm {
+  init?(name: String) {
+    self.init(name: name[...])
   }
 
-  internal enum Algorithm: String {
-    case identity
-    case deflate
-    case gzip
+  init?(name: Substring) {
+    switch name {
+    case "gzip":
+      self = .gzip
+    case "deflate":
+      self = .deflate
+    case "identity":
+      self = .none
+    default:
+      return nil
+    }
   }
 
-  internal let algorithm: Algorithm
-
-  private init(_ algorithm: Algorithm) {
-    self.algorithm = algorithm
+  var name: String {
+    switch self.value {
+    case .gzip:
+      return "gzip"
+    case .deflate:
+      return "deflate"
+    case .none:
+      return "identity"
+    }
   }
+}
 
-  internal init?(rawValue: String) {
-    guard let algorithm = Algorithm(rawValue: rawValue) else {
-      return nil
-    }
-    self.algorithm = algorithm
+extension CompressionAlgorithmSet {
+  var count: Int {
+    self.rawValue.nonzeroBitCount
   }
 }

+ 46 - 42
Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift

@@ -32,12 +32,29 @@ enum GRPCStreamStateMachineConfiguration {
     var methodDescriptor: MethodDescriptor
     var scheme: Scheme
     var outboundEncoding: CompressionAlgorithm
-    var acceptedEncodings: [CompressionAlgorithm]
+    var acceptedEncodings: CompressionAlgorithmSet
+
+    init(
+      methodDescriptor: MethodDescriptor,
+      scheme: Scheme,
+      outboundEncoding: CompressionAlgorithm,
+      acceptedEncodings: CompressionAlgorithmSet
+    ) {
+      self.methodDescriptor = methodDescriptor
+      self.scheme = scheme
+      self.outboundEncoding = outboundEncoding
+      self.acceptedEncodings = acceptedEncodings.union(.none)
+    }
   }
 
   struct ServerConfiguration {
     var scheme: Scheme
-    var acceptedEncodings: [CompressionAlgorithm]
+    var acceptedEncodings: CompressionAlgorithmSet
+
+    init(scheme: Scheme, acceptedEncodings: CompressionAlgorithmSet) {
+      self.scheme = scheme
+      self.acceptedEncodings = acceptedEncodings.union(.none)
+    }
   }
 }
 
@@ -490,7 +507,7 @@ extension GRPCStreamStateMachine {
     methodDescriptor: MethodDescriptor,
     scheme: Scheme,
     outboundEncoding: CompressionAlgorithm?,
-    acceptedEncodings: [CompressionAlgorithm],
+    acceptedEncodings: CompressionAlgorithmSet,
     customMetadata: Metadata
   ) -> HPACKHeaders {
     var headers = HPACKHeaders()
@@ -509,12 +526,12 @@ extension GRPCStreamStateMachine {
     headers.add(ContentType.grpc.canonicalValue, forKey: .contentType)
     headers.add("trailers", forKey: .te)  // Used to detect incompatible proxies
 
-    if let encoding = outboundEncoding, encoding != .identity {
+    if let encoding = outboundEncoding, encoding != .none {
       headers.add(encoding.name, forKey: .encoding)
     }
 
-    for acceptedEncoding in acceptedEncodings {
-      headers.add(acceptedEncoding.name, forKey: .acceptEncoding)
+    for encoding in acceptedEncodings.elements.filter({ $0 != .none }) {
+      headers.add(encoding.name, forKey: .acceptEncoding)
     }
 
     for metadataPair in customMetadata {
@@ -711,7 +728,7 @@ extension GRPCStreamStateMachine {
   ) -> ProcessInboundEncodingResult {
     let inboundEncoding: CompressionAlgorithm
     if let serverEncoding = headers.first(name: GRPCHTTP2Keys.encoding.rawValue) {
-      guard let parsedEncoding = CompressionAlgorithm(rawValue: serverEncoding),
+      guard let parsedEncoding = CompressionAlgorithm(name: serverEncoding),
         configuration.acceptedEncodings.contains(parsedEncoding)
       else {
         return .error(
@@ -727,7 +744,7 @@ extension GRPCStreamStateMachine {
       }
       inboundEncoding = parsedEncoding
     } else {
-      inboundEncoding = .identity
+      inboundEncoding = .none
     }
     return .success(inboundEncoding)
   }
@@ -997,11 +1014,11 @@ extension GRPCStreamStateMachine {
     headers.add("200", forKey: .status)
     headers.add(ContentType.grpc.canonicalValue, forKey: .contentType)
 
-    if let outboundEncoding, outboundEncoding != .identity {
+    if let outboundEncoding, outboundEncoding != .none {
       headers.add(outboundEncoding.name, forKey: .encoding)
     }
 
-    for acceptedEncoding in configuration.acceptedEncodings {
+    for acceptedEncoding in configuration.acceptedEncodings.elements.filter({ $0 != .none }) {
       headers.add(acceptedEncoding.name, forKey: .acceptEncoding)
     }
 
@@ -1241,10 +1258,6 @@ extension GRPCStreamStateMachine {
         )
       }
 
-      func isIdentityOrCompatibleEncoding(_ clientEncoding: CompressionAlgorithm) -> Bool {
-        clientEncoding == .identity || configuration.acceptedEncodings.contains(clientEncoding)
-      }
-
       // Firstly, find out if we support the client's chosen encoding, and reject
       // the RPC if we don't.
       let inboundEncoding: CompressionAlgorithm
@@ -1263,30 +1276,21 @@ extension GRPCStreamStateMachine {
           return .rejectRPC(trailers: trailers)
         }
 
-        guard let clientEncoding = CompressionAlgorithm(rawValue: String(rawEncoding)),
-          isIdentityOrCompatibleEncoding(clientEncoding)
+        guard let clientEncoding = CompressionAlgorithm(name: rawEncoding),
+          configuration.acceptedEncodings.contains(clientEncoding)
         else {
-          let statusMessage: String
-          let customMetadata: Metadata?
-          if configuration.acceptedEncodings.isEmpty {
-            statusMessage = "Compression is not supported"
-            customMetadata = nil
-          } else {
-            statusMessage = """
-              \(rawEncoding) compression is not supported; \
-              supported algorithms are listed in grpc-accept-encoding
-              """
-            customMetadata = {
-              var trailers = Metadata()
-              trailers.reserveCapacity(configuration.acceptedEncodings.count)
-              for acceptedEncoding in configuration.acceptedEncodings {
-                trailers.addString(
-                  acceptedEncoding.name,
-                  forKey: GRPCHTTP2Keys.acceptEncoding.rawValue
-                )
-              }
-              return trailers
-            }()
+          let statusMessage = """
+            \(rawEncoding) compression is not supported; \
+            supported algorithms are listed in grpc-accept-encoding
+            """
+
+          var customMetadata = Metadata()
+          customMetadata.reserveCapacity(configuration.acceptedEncodings.count)
+          for acceptedEncoding in configuration.acceptedEncodings.elements {
+            customMetadata.addString(
+              acceptedEncoding.name,
+              forKey: GRPCHTTP2Keys.acceptEncoding.rawValue
+            )
           }
 
           let trailers = self.makeTrailers(
@@ -1300,12 +1304,12 @@ extension GRPCStreamStateMachine {
         // Server supports client's encoding.
         inboundEncoding = clientEncoding
       } else {
-        inboundEncoding = .identity
+        inboundEncoding = .none
       }
 
       // Secondly, find a compatible encoding the server can use to compress outbound messages,
       // based on the encodings the client has advertised.
-      var outboundEncoding: CompressionAlgorithm = .identity
+      var outboundEncoding: CompressionAlgorithm = .none
       let clientAdvertisedEncodings = headers.values(
         forHeader: GRPCHTTP2Keys.acceptEncoding.rawValue,
         canonicalForm: true
@@ -1314,8 +1318,8 @@ extension GRPCStreamStateMachine {
       // If it's identity, just skip it altogether, since we won't be
       // compressing.
       for clientAdvertisedEncoding in clientAdvertisedEncodings {
-        if let algorithm = CompressionAlgorithm(rawValue: String(clientAdvertisedEncoding)),
-          isIdentityOrCompatibleEncoding(algorithm)
+        if let algorithm = CompressionAlgorithm(name: clientAdvertisedEncoding),
+          configuration.acceptedEncodings.contains(algorithm)
         {
           outboundEncoding = algorithm
           break
@@ -1510,7 +1514,7 @@ extension HPACKHeaders {
 extension Zlib.Method {
   init?(encoding: CompressionAlgorithm) {
     switch encoding {
-    case .identity:
+    case .none:
       return nil
     case .deflate:
       self = .deflate

+ 1 - 1
Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift

@@ -39,7 +39,7 @@ final class GRPCServerStreamHandler: ChannelDuplexHandler {
 
   init(
     scheme: Scheme,
-    acceptedEncodings: [CompressionAlgorithm],
+    acceptedEncodings: CompressionAlgorithmSet,
     maximumPayloadSize: Int,
     skipStateMachineAssertions: Bool = false
   ) {

+ 11 - 11
Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift

@@ -30,7 +30,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 1
     )
@@ -60,7 +60,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 1,
       skipStateMachineAssertions: true
@@ -96,7 +96,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 1,
       skipStateMachineAssertions: true
@@ -127,7 +127,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 1,
       skipStateMachineAssertions: true
@@ -164,7 +164,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 1,
       skipStateMachineAssertions: true
@@ -258,7 +258,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 1,
       skipStateMachineAssertions: true
@@ -328,7 +328,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 100,
       skipStateMachineAssertions: true
@@ -396,7 +396,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 1,
       skipStateMachineAssertions: true
@@ -462,7 +462,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 100,
       skipStateMachineAssertions: true
@@ -580,7 +580,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 100,
       skipStateMachineAssertions: true
@@ -685,7 +685,7 @@ final class GRPCClientStreamHandlerTests: XCTestCase {
     let handler = GRPCClientStreamHandler(
       methodDescriptor: .init(service: "test", method: "test"),
       scheme: .http,
-      outboundEncoding: .identity,
+      outboundEncoding: .none,
       acceptedEncodings: [],
       maximumPayloadSize: 100,
       skipStateMachineAssertions: true

+ 2 - 1
Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift

@@ -139,7 +139,7 @@ final class GRPCStreamClientStateMachineTests: XCTestCase {
         .init(
           methodDescriptor: .init(service: "test", method: "test"),
           scheme: .http,
-          outboundEncoding: compressionEnabled ? .deflate : .identity,
+          outboundEncoding: compressionEnabled ? .deflate : .none,
           acceptedEncodings: [.deflate]
         )
       ),
@@ -1751,6 +1751,7 @@ final class GRPCStreamServerStateMachineTests: XCTestCase {
           "grpc-status": "12",
           "grpc-status-message":
             "gzip compression is not supported; supported algorithms are listed in grpc-accept-encoding",
+          "grpc-accept-encoding": "identity",
         ]
       )
     }

+ 3 - 1
Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift

@@ -266,7 +266,9 @@ final class GRPCServerStreamHandlerTests: XCTestCase {
         GRPCHTTP2Keys.status.rawValue: "200",
         GRPCHTTP2Keys.contentType.rawValue: "application/grpc",
         GRPCHTTP2Keys.grpcStatus.rawValue: String(Status.Code.unimplemented.rawValue),
-        GRPCHTTP2Keys.grpcStatusMessage.rawValue: "Compression is not supported",
+        GRPCHTTP2Keys.grpcStatusMessage.rawValue:
+          "deflate compression is not supported; supported algorithms are listed in grpc-accept-encoding",
+        GRPCHTTP2Keys.acceptEncoding.rawValue: "identity",
       ]
     )
     XCTAssertTrue(writtenTrailersOnlyResponse.endStream)