From af55765dff765f46d1e8c030be00c8a6edea5292 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Fri, 12 Apr 2024 16:50:56 +0100 Subject: [PATCH 1/3] Move CompressionAlgorithm to core 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 --- .../GRPCCore/Call/Client/CallOptions.swift | 46 +++---- .../Coding/CompressionAlgorithm.swift | 129 ++++++++++++++++++ .../Client/GRPCClientStreamHandler.swift | 2 +- .../Compression/CompressionAlgorithm.swift | 55 ++++---- .../GRPCStreamStateMachine.swift | 88 ++++++------ .../Server/GRPCServerStreamHandler.swift | 2 +- .../Coding/CompressionAlgorithmTests.swift | 61 +++++++++ .../Client/GRPCClientStreamHandlerTests.swift | 22 +-- .../GRPCStreamStateMachineTests.swift | 3 +- .../Server/GRPCServerStreamHandlerTests.swift | 4 +- 10 files changed, 304 insertions(+), 108 deletions(-) create mode 100644 Sources/GRPCCore/Coding/CompressionAlgorithm.swift create mode 100644 Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index c73b6af73..3a6385227 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -80,33 +80,33 @@ public struct CallOptions: Sendable { /// Whether compression is enabled or not for request and response messages. public var compression: Compression + /// Compression configuration. + /// + /// Note that this configuration is advisory: not all transports support compression and may + /// ignore this configuration. Transports which support compression will use this configuration + /// in preference to any compression configured at a transport level. public struct Compression: Sendable { - /// Whether request messages should be compressed. + /// The algorithm used for compressing outbound messages. /// - /// Note that this option is _advisory_: transports are not required to support compression. - public var requests: Bool + /// If `nil` the value configured on the transport will be used instead. + public var algorithm: CompressionAlgorithm? - /// Whether response messages are permitted to be compressed. - public var responses: Bool + /// The enabled compression algorithms. + /// + /// If `nil` the value configured on the transport will be used instead. + public var enabledAlgorithms: CompressionAlgorithmSet? /// Creates a new ``Compression`` configuration. /// /// - Parameters: - /// - requests: Whether request messages should be compressed. - /// - responses: Whether response messages may be compressed. - public init(requests: Bool, responses: Bool) { - self.requests = requests - self.responses = responses - } - - /// Sets ``requests`` and ``responses`` to `true`. - public static var enabled: Self { - Self(requests: true, responses: true) - } - - /// Sets ``requests`` and ``responses`` to `false`. - public static var disabled: Self { - Self(requests: false, responses: false) + /// - algorithm: The algorithm used for compressing outbound messages. + /// - enabledAlgorithms: The enabled compression algorithms. + public init( + algorithm: CompressionAlgorithm? = nil, + enabledAlgorithms: CompressionAlgorithmSet? = nil + ) { + self.algorithm = algorithm + self.enabledAlgorithms = enabledAlgorithms } } @@ -131,9 +131,7 @@ public struct CallOptions: Sendable { extension CallOptions { /// Default call options. /// - /// The default values defer values to the underlying transport. In most cases this means values - /// are `nil`, with the exception of ``compression-swift.property`` which is set - /// to ``Compression-swift.struct/disabled``. + /// The default values (`nil`) defer values to the underlying transport. public static var defaults: Self { Self( timeout: nil, @@ -141,7 +139,7 @@ extension CallOptions { maxRequestMessageBytes: nil, maxResponseMessageBytes: nil, executionPolicy: nil, - compression: .disabled + compression: Compression() ) } } diff --git a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift new file mode 100644 index 000000000..830abe0c9 --- /dev/null +++ b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift @@ -0,0 +1,129 @@ +/* + * Copyright 2024, 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. + */ + +/// Message compression algorithms. +public struct CompressionAlgorithm: Hashable, Sendable { + @_spi(Package) + public enum Value: UInt8, Hashable, Sendable, CaseIterable { + case none = 0 + case deflate + case gzip + } + + @_spi(Package) + public let value: Value + + fileprivate init(_ algorithm: Value) { + self.value = algorithm + } + + /// No compression, sometimes referred to as 'identity' compression. + public static var none: Self { + Self(.none) + } + + /// The 'deflate' compression algorithm. + public static var deflate: Self { + Self(.deflate) + } + + /// The 'gzip' compression algorithm. + public static var gzip: Self { + Self(.gzip) + } +} + +/// A set of compression algorithms. +public struct CompressionAlgorithmSet: OptionSet, Hashable, Sendable { + public var rawValue: UInt32 + + public init(rawValue: UInt32) { + self.rawValue = rawValue + } + + private init(value: CompressionAlgorithm.Value) { + self.rawValue = 1 << value.rawValue + } + + /// No compression, sometimes referred to as 'identity' compression. + public static var none: Self { + return Self(value: .none) + } + + /// The 'deflate' compression algorithm. + public static var deflate: Self { + return Self(value: .deflate) + } + + /// The 'gzip' compression algorithm. + public static var gzip: Self { + return Self(value: .gzip) + } + + /// All compression algorithms. + public static var all: Self { + return [.gzip, .deflate, .none] + } + + /// Returns whether a given algorithm is present in the set. + /// + /// - Parameter algorithm: The algorithm to check. + public func contains(_ algorithm: CompressionAlgorithm) -> Bool { + return self.contains(CompressionAlgorithmSet(value: algorithm.value)) + } +} + +extension CompressionAlgorithmSet { + /// A sequence of ``CompressionAlgorithm`` values present in the set.. + public var elements: Elements { + Elements(algorithmSet: self) + } + + /// A sequence of ``CompressionAlgorithm`` values present in a ``CompressionAlgorithmSet``. + public struct Elements: Sequence { + public typealias Element = CompressionAlgorithm + + private let algorithmSet: CompressionAlgorithmSet + + init(algorithmSet: CompressionAlgorithmSet) { + self.algorithmSet = algorithmSet + } + + public func makeIterator() -> Iterator { + return Iterator(algorithmSet: self.algorithmSet) + } + + public struct Iterator: IteratorProtocol { + private let algorithmSet: CompressionAlgorithmSet + private var iterator: [CompressionAlgorithm.Value].Iterator + + init(algorithmSet: CompressionAlgorithmSet) { + self.algorithmSet = algorithmSet + self.iterator = CompressionAlgorithm.Value.allCases.makeIterator() + } + + public mutating func next() -> CompressionAlgorithm? { + while let value = self.iterator.next() { + if self.algorithmSet.contains(CompressionAlgorithmSet(value: value)) { + return CompressionAlgorithm(value) + } + } + + return nil + } + } + } +} diff --git a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift b/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift index 18e36ab26..e03693995 100644 --- a/Sources/GRPCHTTP2Core/Client/GRPCClientStreamHandler.swift +++ b/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 ) { diff --git a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift b/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift index 82de83eed..7401fc8c3 100644 --- a/Sources/GRPCHTTP2Core/Compression/CompressionAlgorithm.swift +++ b/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 } } diff --git a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift b/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift index 4554f863e..c063b9030 100644 --- a/Sources/GRPCHTTP2Core/GRPCStreamStateMachine.swift +++ b/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 diff --git a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift b/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift index 3624679e9..4a19e22f8 100644 --- a/Sources/GRPCHTTP2Core/Server/GRPCServerStreamHandler.swift +++ b/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 ) { diff --git a/Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift b/Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift new file mode 100644 index 000000000..351538816 --- /dev/null +++ b/Tests/GRPCCoreTests/Coding/CompressionAlgorithmTests.swift @@ -0,0 +1,61 @@ +/* + * Copyright 2024, 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 GRPCCore +import XCTest + +final class CompressionAlgorithmTests: XCTestCase { + func testCompressionAlgorithmSetContains() { + var algorithms = CompressionAlgorithmSet() + XCTAssertFalse(algorithms.contains(.gzip)) + XCTAssertFalse(algorithms.contains(.deflate)) + XCTAssertFalse(algorithms.contains(.none)) + + algorithms.formUnion(.gzip) + XCTAssertTrue(algorithms.contains(.gzip)) + XCTAssertFalse(algorithms.contains(.deflate)) + XCTAssertFalse(algorithms.contains(.none)) + + algorithms.formUnion(.deflate) + XCTAssertTrue(algorithms.contains(.gzip)) + XCTAssertTrue(algorithms.contains(.deflate)) + XCTAssertFalse(algorithms.contains(.none)) + + algorithms.formUnion(.none) + XCTAssertTrue(algorithms.contains(.gzip)) + XCTAssertTrue(algorithms.contains(.deflate)) + XCTAssertTrue(algorithms.contains(.none)) + } + + func testCompressionAlgorithmSetElements() { + var algorithms = CompressionAlgorithmSet.all + XCTAssertEqual(Array(algorithms.elements), [.none, .deflate, .gzip]) + + algorithms.subtract(.deflate) + XCTAssertEqual(Array(algorithms.elements), [.none, .gzip]) + + algorithms.subtract(.none) + XCTAssertEqual(Array(algorithms.elements), [.gzip]) + + algorithms.subtract(.gzip) + XCTAssertEqual(Array(algorithms.elements), []) + } + + func testCompressionAlgorithmSetElementsIgnoresUnknownBits() { + let algorithms = CompressionAlgorithmSet(rawValue: .max) + XCTAssertEqual(Array(algorithms.elements), [.none, .deflate, .gzip]) + } +} diff --git a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift index 6e7d68acd..b8ea4f59f 100644 --- a/Tests/GRPCHTTP2CoreTests/Client/GRPCClientStreamHandlerTests.swift +++ b/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 diff --git a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift b/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift index a886d0053..dc7f4aa96 100644 --- a/Tests/GRPCHTTP2CoreTests/GRPCStreamStateMachineTests.swift +++ b/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", ] ) } diff --git a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift b/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift index 9d56824be..e05be5d8e 100644 --- a/Tests/GRPCHTTP2CoreTests/Server/GRPCServerStreamHandlerTests.swift +++ b/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) From 68b63beac1b66360bd419c0a94565b8718365d72 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Mon, 15 Apr 2024 09:32:09 +0100 Subject: [PATCH 2/3] fixup --- Sources/GRPCCore/Coding/CompressionAlgorithm.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift index 830abe0c9..30c119ccc 100644 --- a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift +++ b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift @@ -108,7 +108,7 @@ extension CompressionAlgorithmSet { public struct Iterator: IteratorProtocol { private let algorithmSet: CompressionAlgorithmSet - private var iterator: [CompressionAlgorithm.Value].Iterator + private var iterator: IndexingIterator<[CompressionAlgorithm.Value]> init(algorithmSet: CompressionAlgorithmSet) { self.algorithmSet = algorithmSet From 85e5289024c39dad57e3a35204107ebdeddc46d9 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 16 Apr 2024 08:34:39 +0100 Subject: [PATCH 3/3] Simplify --- .../GRPCCore/Call/Client/CallOptions.swift | 43 ++++++------------- .../Coding/CompressionAlgorithm.swift | 2 +- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/CallOptions.swift b/Sources/GRPCCore/Call/Client/CallOptions.swift index 3a6385227..b9aaba59e 100644 --- a/Sources/GRPCCore/Call/Client/CallOptions.swift +++ b/Sources/GRPCCore/Call/Client/CallOptions.swift @@ -77,38 +77,19 @@ public struct CallOptions: Sendable { /// reported to the client. Hedging is only suitable for idempotent RPCs. public var executionPolicy: RPCExecutionPolicy? - /// Whether compression is enabled or not for request and response messages. - public var compression: Compression - - /// Compression configuration. + /// The compression used for the call. + /// + /// Compression in gRPC is asymmetrical: the server may compress response messages using a + /// different algorithm than the client used to compress request messages. This configuration + /// controls the compression used by the client for request messages. /// /// Note that this configuration is advisory: not all transports support compression and may /// ignore this configuration. Transports which support compression will use this configuration - /// in preference to any compression configured at a transport level. - public struct Compression: Sendable { - /// The algorithm used for compressing outbound messages. - /// - /// If `nil` the value configured on the transport will be used instead. - public var algorithm: CompressionAlgorithm? - - /// The enabled compression algorithms. - /// - /// If `nil` the value configured on the transport will be used instead. - public var enabledAlgorithms: CompressionAlgorithmSet? - - /// Creates a new ``Compression`` configuration. - /// - /// - Parameters: - /// - algorithm: The algorithm used for compressing outbound messages. - /// - enabledAlgorithms: The enabled compression algorithms. - public init( - algorithm: CompressionAlgorithm? = nil, - enabledAlgorithms: CompressionAlgorithmSet? = nil - ) { - self.algorithm = algorithm - self.enabledAlgorithms = enabledAlgorithms - } - } + /// in preference to the algorithm configured at a transport level. If the transport hasn't + /// enabled the use of the algorithm then compression won't be used for the call. + /// + /// If `nil` the value configured on the transport will be used instead. + public var compression: CompressionAlgorithm? internal init( timeout: Duration?, @@ -116,7 +97,7 @@ public struct CallOptions: Sendable { maxRequestMessageBytes: Int?, maxResponseMessageBytes: Int?, executionPolicy: RPCExecutionPolicy?, - compression: Compression + compression: CompressionAlgorithm? ) { self.timeout = timeout self.waitForReady = waitForReady @@ -139,7 +120,7 @@ extension CallOptions { maxRequestMessageBytes: nil, maxResponseMessageBytes: nil, executionPolicy: nil, - compression: Compression() + compression: nil ) } } diff --git a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift index 30c119ccc..e3b39e048 100644 --- a/Sources/GRPCCore/Coding/CompressionAlgorithm.swift +++ b/Sources/GRPCCore/Coding/CompressionAlgorithm.swift @@ -87,7 +87,7 @@ public struct CompressionAlgorithmSet: OptionSet, Hashable, Sendable { } extension CompressionAlgorithmSet { - /// A sequence of ``CompressionAlgorithm`` values present in the set.. + /// A sequence of ``CompressionAlgorithm`` values present in the set. public var elements: Elements { Elements(algorithmSet: self) }